How to quickly setup a GraphQL server in Elixir using Abinsthe

GraphQL in Elixir

Elixir is an excellent choice for a GraphQL backend. It has good enough performance and concurrency to handle a large volume of requests without caching support, which is useful considering that request-level caching is not possible with GraphQL like it is with REST.

The Elixir ecosystem is also blessed with what is in my opinion one of the best GraphQL DSLs around - Absinthe.

Absinthe is a collection of libraries to help with parsing and responding to GraphQL queries. It can run standalone or on top of Phoenix. The easiest way to get started is with Phoenix, so let’s dive right in.

Building a basic GraphQL server with Phoenix/Absinthe

Let’s say I’m building an MMA fan site. I want to be able to get information about the fighters from the backend for both the website (written in React) and a mobile app.

Initial setup

Create a new phoenix project with $ mix phx.new mma --database postgres --no-brunch --no-html.

You’ll need to add the absinthe dependencies to your mix.exs

defp deps do
  [
    {:phoenix, "~> 1.3.0"},
    {:phoenix_pubsub, "~> 1.0"},
    {:phoenix_ecto, "~> 3.2"},
    {:postgrex, ">= 0.0.0"},
    {:gettext, "~> 0.11"},
    {:cowboy, "~> 1.0"},

    # Absinthe
    {:absinthe, "~> 1.3.0"},
    {:absinthe_ecto, "~> 0.1.2"},
    {:absinthe_plug, "~> 1.3.0"}
  ]
end

Then run $ mix deps.get to install.

Create Fighters and Fights

I’ve opted to create three records to show the use of associations in GraphQL. Our API shows Fighters with their vital statistics, and a list of their past fights including whether they won or lost.

$ mix phx.gen.schema Fighter fighters name:string belts:integer weight_in_kilos:float

$ mix phx.gen.schema Fight fights name:string

$ mix phx.gen.schema FightResult fight_results fight_id:references:fights fighter_id:references:fighters result:string

$ mix ecto.create

$ mix ecto.migrate

Let’s populate it with some data for testing.

# priv/repo/seeds.exs

### Fighters

conor = Mma.Repo.insert!(%Mma.Fighter{
  name: "Conor McGregor",
  belts: 2,
  weight_in_kilos: 69.4
})

jon = Mma.Repo.insert!(%Mma.Fighter{
  name: "Jon Jones",
  belts: 0,
  weight_in_kilos: 92.99
})

daniel = Mma.Repo.insert!(%Mma.Fighter{
  name: "Daniel Cormier",
  belts: 1,
  weight_in_kilos: 92.99
})

Mma.Repo.insert!(%Mma.Fighter{
  name: "Demetrious \"Mighty Mouse\" Johnson",
  belts: 1,
  weight_in_kilos: 56.7
})

### Fights

ufc182 = Mma.Repo.insert!(%Mma.Fight{
  name: "UFC 182"
})

ufc214 = Mma.Repo.insert!(%Mma.Fight{
  name: "UFC 214"
})

mcgregor_mayweather = Mma.Repo.insert!(%Mma.Fight{
  name: "McGregor vs. Mayweather"
})

### FightResults

Mma.Repo.insert!(%Mma.FightResult{
  fight_id: ufc182.id,
  fighter_id: jon.id,
  result: "Win"
})

Mma.Repo.insert!(%Mma.FightResult{
  fight_id: ufc182.id,
  fighter_id: daniel.id,
  result: "Loss"
})

Mma.Repo.insert!(%Mma.FightResult{
  fight_id: ufc214.id,
  fighter_id: jon.id,
  result: "Win"
})

Mma.Repo.insert!(%Mma.FightResult{
  fight_id: ufc214.id,
  fighter_id: daniel.id,
  result: "Loss"
})

Mma.Repo.insert!(%Mma.FightResult{
  fight_id: mcgregor_mayweather.id,
  fighter_id: conor.id,
  result: "Loss"
})

Run $ mix run priv/repo/seeds.exs to fill your dev database with seed data.

Add relations

# fighter.ex

schema "fighters" do
  has_many :fight_results, Mma.FightResult
  ...
# fight_result.ex

schema "fight_results" do
  belongs_to :fight, Mma.Fight
  ...

Define our GraphQL Types

GraphQL types represent a tree of objects with scalars at the leaves.

Create a new file at lib/mma_web/schema/types.ex and add the following code:

defmodule MmaWeb.Schema.Types do
  use Absinthe.Schema.Notation
  use Absinthe.Ecto, repo: Mma.Repo

  object :fighter do
    field :id, :id
    field :belts, :integer
    field :name, :string
    field :weight_in_kilos, :float
    field :fight_results, list_of(:fight_result), resolve: assoc(:fight_results)
  end

  object :fight_result do
    field :result, :string
    field :fight, :fight, resolve: assoc(:fight)
  end

  object :fight do
    field :name, :string
  end
end

You’ll notice the schema closely mirrors our database schema. This is quite normal when working with Absinthe and Ecto relations.

By default Absinthe will attempt to look up the keys in the resolved struct. Since fight_results is empty on a freshly loaded Mma.Fighter we’ll need to tell Absinthe how to load it.

assoc is a function that comes from Absinthe.Ecto and specifies how to load data from associations. It automatically batches queries to avoid N+1 queries.

Create our GraphQL Schema

A GraphQL schema describes relationships between objects and exposes queries and mutations for accessing them.

Create a new file at lib/mma_web/schema.ex containing:

defmodule MmaWeb.Schema do
  use Absinthe.Schema
  import_types MmaWeb.Schema.Types

  query do
    field :fighters, list_of(:fighter) do
      resolve fn _params, _info ->
        {:ok, Mma.Repo.all(Mma.Fighter)}
      end
    end
  end
end

Explore using GraphiQL

absinthe_plug comes with an awesome tool called GraphiQL that can be used to test and explore your GraphQL queries.

To enable it, open lib/mma_web/router.ex and add the following lines:

  forward "/graphiql",
    Absinthe.Plug.GraphiQL,
    schema: MmaWeb.Schema

Boot your server with mix phx.server and visit localhost:4000/graphiql.

You can introspect your schema and see automatically generated documentation using the Docs tab on the right hand side of the page.

You can query basic fighter information like this:

query FightersName {
  fighters {
    id
    name
    weightInKilos
  }
}

Note that GraphiQL is able to autocomplete fields, and Absinthe has automagically camelCased them for us. You can use snake_case in your queries as well and absinthe will seamlessly convert between the two.

To get more information, we can construct a more detailed query based on the types we defined. Associations are automatically loaded by absinthe_ecto.

query FightersWithFights {
  fighters {
    id
    belts
    fightResults {
      result
      fight {
        name
      }
    }
    name
    weightInKilos
  }
}

And there you have it, a fully functional GraphQL API. Note how little code we had to write to get here!