Using GraphQL with Lucky
Tags: lucky, crystal, graphql
Published: 2021-04-07 01:22:08

---
title: Using GraphQL with Lucky
published: true
description: How to integrate GraphQL with the Lucky Framework
tags: lucky, crystal, graphql
//cover_image: https://direct_url_to_image.jpg
---

This is how to get [GraphQL](https://graphql.org/) running with [Lucky Framework](https://luckyframework.org/).

### Preface

I have a total of just 1 app that uses GraphQL under my belt, so I'm by no means an expert. Chances are, this setup is "bad" in terms of using GraphQL; however, it's working... so with that said, here's how I got it running.

## Setup

We need to get our Lucky app setup first. We can use a quick shortcut and skip the wizard :grimacing:

```bash
lucky init.custom lucky_graph
cd lucky_graph
# Edit your config/database.cr if you need
```

Before we run the `setup` script, we need to add our dependencies. We will add the [GraphQL shard](https://github.com/graphql-crystal/graphql).

```yml
# shard.yml
dependencies:
  graphql:
    github: graphql-crystal/graphql
    branch: master
```

Ok, now we can run our `./script/setup` to install our shards, setup the DB, and all that fun stuff. Do that now.... 

```
./script/setup
```

Then require the GraphQL shard require to your `./src/shards.cr`

```crystal
# ...
require "avram"
require "lucky"
# ...
require "graphql"
```

Lastly, before we go writing some code, let's generate our graph action.

```
lucky gen.action.api Api::Graphql::Index
```

This will generate a new action in your `./src/actions/api/graphql/index.cr`.

## Graph Action

We generated an "index" file, but GraphQL does `POST` requests... it's not quite "REST", but that's the whole point, right? :sweat_smile:

Let's open up that new action file, and update to work our GraphQL.

```crystal
# src/actions/api/graphql/index.cr
class Api::Graphql::Index < ApiAction
  # NOTE: This is only for a test. I'll come back to it later
  include Api::Auth::SkipRequireAuthToken
  param query : String

  post "/api/graphql" do
    send_text_response(
      schema.execute(query, variables, operation_name, Graph::Context.new(current_user?)),
      "application/json",
      200
    )
  end

  private def schema
    GraphQL::Schema.new(Graph::Queries.new, Graph::Mutations.new)
  end

  private def operation_name
    params.from_json["operationName"].as_s
  end

  private def variables
    params.from_json["variables"].as_h
  end
end
```

There's a few things going on here, so I'll break them down.

### send_text_response

It's true Lucky has a `json()` response method, but that method takes an object and calls `to_json` on it. In our case, the `schema.execute()` will return a json string. So passing that in to `json()` would result in a super escaped json object string `"{\"key\":\"val\"}"`. We can use `send_text_response`, and tell it to return a json content-type.

### param query

When we make our GraphQL call from the front-end, our query will be the full formatted query (or mutation).

### operation_name and variables

When you send the GraphQL POST from your client, it might look something like this:

```json
{"operationName":"FeaturedPosts",
 "variables":{"limit":20},
 "query":"query FeaturedPosts($limit: Integer!) {
  posts(featured: true, limit: $limit) {
    title
    slug
    content
    publishedAt
  }
 }"
}
```

We can pull out the `operationName`, and the `variables` allowing the GraphQL shard to do some magic behind the scenes.

### A few extra classes

We have a few calls to some classes that don't exist, yet. We will need to add these next.

* `Graph::Context` - A class that will contain access to our `current_user`
* `Graph::Queries` - A class where we will define what our graphql queries will do
* `Graph::Mutations` - A class where we will define what our graphql mutations will do

## Graph objects

In GraphQL, you'll have all kinds of different objects to interact with. It's really its own mini little framework. You might have input objects, outcome objects, or possibly breaking your logic out in to mini bits. We can put all of this in to a new `src/graph/` directory.

```
mkdir ./src/graph
```

Then make sure to require the new `graph/` directory in `./src/app.cr`.

```crystal
# ./src/app.cr
require "./shards"

# ...
require "./app_database"
require "./models/base_model"
# ...
require "./serializers/base_serializer"
require "./serializers/**"

# This should go here
# After your Models, Operations, Queries, Serializers
# but before Actions, Pages, Components, etc...
require "./graph/*"

# ...
require "./actions/**"
# ...
require "./app_server"
```

Next we will create all of the new Graph objects we will be using.

### Graph::Context

Create a new file in `./src/graph/context.cr`

```crystal
# src/graph/context.cr
class Graph::Context < GraphQL::Context
  property current_user : User?

  def initialize(@current_user)
  end
end
```

### Graph::Queries

The `Graph::Queries` object should contain methods that fetch data from the database. Generally these will use a Query object from your `./src/queries/` directory, or just piggy back off the `current_user` object as needed.

Create a new file in `./src/graph/queries.cr`

```crystal
# src/graph/queries.cr
@[GraphQL::Object]
class Graph::Queries
  include GraphQL::ObjectType
  include GraphQL::QueryType

  @[GraphQL::Field]
  def me(context : Graph::Context) : UserSerializer?
    if user = context.current_user
      UserSerializer.new(user)
    end
  end
end
```

This query object starts with a single method `me` which will return a serialized version of the `current_user` if there is a `current_user`. You'll notice all of the annotations. This GraphQL shard LOVES the annotations :joy:

For our queries to return a `Lucky::Serializer` object like `UserSerializer`, we'll need to update it and tell it that it's a GraphQL object.

Open up `./src/serializers/user_serializer.cr`

```diff
# src/serializers/user_serializer.cr
+ @[GraphQL::Object]
  class UserSerializer < BaseSerializer
+   include GraphQL::ObjectType

    def initialize(@user : User)
    end

    def render
-     {email: @user.email}
    end

+   @[GraphQL::Field]
+   def email : String
+     @user.email
+   end
  end
```

> That include could probably go in your `BaseSerializer` if you wanted.

### Graph::Mutations

The `Graph::Mutations` object should contain methods that mutate the data (i.e. create, update, destroy). Generally these will call to your Operation objects from your `./src/operations/` directory.

Create a new file in `./src/graph/mutations.cr`

```crystal
# src/graph/mutations.cr
@[GraphQL::Object]
class Graph::Mutations
  include GraphQL::ObjectType
  include GraphQL::MutationType

  @[GraphQL::Field]
  def login(email : String, password : String) : MutationOutcome
    outcome = MutationOutcome.new(success: false)

    SignInUser.run(
      email: email,
      password: password
    ) do |operation, authenticated_user|
      if authenticated_user
        outcome.success = true
      else
        outcome.errors = operation.errors.to_json
      end
    end

    outcome
  end
end
```

Notice the `MutationOutcome` object here. We haven't created this yet, or mentioned it. The GraphQL shard requires that all of the methods have a return type signature, and that type has to be some supported object. This is just an example of what you could do, but really, the return object is up to you. You can have it return a `UserSerializer?` as well if you wanted.

### MutationOutcome

The idea here is that we have some sort of generic object. It has two properties `success : Bool` and `errors : String?`.

Create this file in `./src/graph/outcomes/mutation_outcome.cr`.

```crystal
# src/graph/outcomes/mutation_outcome.cr
@[GraphQL::Object]
class MutationOutcome
  include GraphQL::ObjectType

  setter success : Bool = false
  setter errors : String?

  @[GraphQL::Field]
  def success : Bool
    @success
  end

  @[GraphQL::Field]
  def errors : String?
    @errors
  end
end
```

By putting this in a nested outcomes directory, we can organize other potential outcomes we might want to add. We will need to require this directory right before the rest of the graph.

```crystal
# update src/app.cr
require "./graph/outcomes/*"
require "./graph/*"
# ...
```

### Checking the code

Before we continue on the client side, let's make sure our app boots and everything is good. We'll need some data in our database to test that our client code works.

Boot the app `lucky dev`. There shouldn't be any compilation errors, but if there are, work through those, and I'll see you when you get back....

Back? Cool. Now that the app is booted, go to your `/sign_up` page, and create an account. For this test, just use the email `test@test.com`, and password `password`. We will update this `/me` page with some code to test that the graph works.

## The Client

Now that the back-end is all setup, all we need to do is hook up the client side to actually make a call to the Graph.

For this code, I'm going to stick to very bare-bones. Everyone has their own preference as to how they want the client end configured, so I'll leave most of it up to you.

### Add a button

Open up `./src/pages/me/show_page.cr`, and add a button

```crystal
# src/pages/me/show_page.cr
class Me::ShowPage < MainLayout
  def content
    h1 "This is your profile"
    h3 "Email:  #{@current_user.email}"

    # Add in this button
    button "Send Test", id: "test-btn"
    helpful_tips
  end
  # ...
end
```

### Adding JS

We will add some setup code to `./src/js/app.js` to get the client configured.

```javascript
// src/js/app.js

require("@rails/ujs").start();
require("turbolinks").start();
// ...

const sendGraphQLTest = ()=> {
  fetch('/api/graphql', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Accept': 'application/json',
    },
    body: JSON.stringify({
      operationName: "Login",
      variables: {email: "test@test.com", password: "password"},
      query: `
        mutation Login($email: String!, $password: String!) {
          login(email: $email, password: $password) {
            success
            errors
          }
        }
      `
    })
  })
  .then(r => r.json())
  .then(data => console.log('data returned:', data));
}


document.addEventListener("turbolinks:load", ()=> {
  const btn = document.querySelector("#test-btn");
  btn.addEventListener("click", sendGraphQLTest);
})
```

Save that, head over to your browser and click the button. In your JS console, you should see an output showing `data.login.success` is `true`!

## Next Steps

Ok, we officially have a client side JS calling some GraphQL back in to Lucky. Obviously the client code isn't flexible, and chances are you're going to use something like [Apollo](https://www.apollographql.com/docs/tutorial/client/) anyway.

Before you go complicating the front-end, give this challenge a try:

1. Remove the `include Api::Auth::SkipRequireAuthToken` from your `Api::Graphql::Index` action.
2. Try to make a query call to `me`.

```
query Me {
  me {
    email
  }
}
```

Notice how you get an error telling you you're unauthorized.
3. Update the `MutationOutcome` to include a `token : String?` property
4. Set the token property to `outcome.token = UserAuthToken.generate(authenticated_user)`.
5. Take the outcome token, and pass that back to make an authenticated call to the query Me.

## Final thoughts

It's a ton of boilerplate, and setup... I get that, and I also think we can make it a lot better. If you have some ideas on making the Lucky / GraphQL connection better, or you see anything in this tutorial that doesn't quite follow a true graph flow, let me know! Come hop in to the [Lucky Discord](https://luckyframework.org/) and we can chat more on how to take this to the next level.

**UPDATE**: It was brought up to me that the Serializer objects should probably move to Graph Type objects. With the serializers, the `render` method is required to be defined, but if you don't have a separate API outside of GraphQL, then that `render` method will never be called. You can remove the inheritence, and the `render` method, and it should all still work!