A Full Stack Guide to Graphql: Elixir Phoenix Server

Jawakar Durai's avatar

Jawakar Durai

This is the second post in the series of GraphQL posts where we started with intro to GraphQl, and then implemented a simple NodeJs GraphQl service to understand the concepts like Resolvers, Schemas (typeDefs),Query and Mutation.

As soon as GraphQl was open-sourced, the community had started to implement the specs in their favorite server-side languages.

With that known, we are going to implement the same GraphQL service in Elixir using Phoenix Framework.

Phoenix Framework

Phoenix is a web framework that implements the popular server-side Model View Controller pattern and it is written in elixir. You should have basic experience in working with Elixir and Phoenix to continue reading.

Absinthe GraphQL

Absinthe GraphQL is a GraphQL implementation in Elixir we are going to use.

Phoenix setup

As Phoenix is written in Elixir, the first step is to install Elixir, then Phoenix.

If you're lazy, like me 🥱 and don't have these installed, just run these commands:

$ brew install asdf
$ brew install postgresql
 
$ KERL_CONFIGURE_OPTIONS="--without-javac --with-ssl=$(brew --prefix openssl)"
 
$ asdf plugin-add erlang https://github.com/asdf-vm/asdf-erlang.git
$ asdf install erlang 22.1
$ asdf global erlang 22.1
 
$ asdf plugin-add elixir https://github.com/asdf-vm/asdf-elixir.git
$ asdf install elixir 1.9
$ asdf global elixir 1.9
 
# To check that we are on Elixir 1.5 and Erlang 18 or later, run:
$ elixir -v
 
$ mix local.hex
$ mix archive.install hex phx_new 1.4.16

Assuming the installation is complete, let's go to our project setup.

Project setup

Create a new Phoenix project:

$ mix phx.new menucard --no-webpack

Create the database and start the server:

$ cd menucard
$ mix ecto.migrate
$ mix phx.server
[info] Running MenuCardWeb.Endpoint with cowboy 2.7.0 at 0.0.0.0:4000 (http)
[info] Access MenuCardWeb.Endpoint at http://localhost:4000

Phoenix app template ships with postgres as default database adapter with postgres user and postgres password.

Now the app is running inside iex session but not interactive(use iex -S mix phx.server to start the interactive session). Stop the app and continue (hit ctrl+c twice).

Absinthe

If you're new to GraphQL, I'd suggest you read the previous article or go through official GraphQL documentation or guide for better knowledge of GraphQL specs.

Absinthe mostly supports all the specs GraphQL supports, we are now going to create the app in Phoenix.

Let's setup Absinthe.

Add absinthe and absinthe_plug as a dependency, absinthe_plug is to use absinthe with phoenix and GraphiQL interface.

# mix.exe

defp deps do
  [
    ..,
    {:absinthe, "~> 1.4"},
    {:absinthe_plug, "~> 1.4"}
  ]
end

And run:

$ mix deps.get

In Absinthe also we define Schema, Resolvers, and Types. We can define them as different modules or in a single file.

Create schema in lib/menu_card_web/schema.ex as MenuCardWeb.Schema

A simple schema for our app can be:

# lib/menu_card_web/schema.ex

defmodule MenuCardWeb.Schema do
  use Absinthe.Schema

  @desc "An item"
  object :item do
    field :id, :id
    field :name, :string
  end

  # Example data
  @menu_items %{
    "foo" => %{id: 1, name: "Pizza"},
    "bar" => %{id: 2, name: "Burger"},
    "foobar" => %{id: 3, name: "PizzaBurger"}
  }

  query do
    field :menu_item, :item do
      arg :id, non_null(:id)
      resolve fn %{id: item_id}, _ ->
        {:ok, @menu_items[item_id]}
      end
    end
  end

end

This is the simple schema where we can query for a specific item.

We are using some macros and functions which are written in Absinthe.Schema.

  • query: rootQuery object macro where we define different queries as fields. There's also equal mutation macro for mutations
  • field: A field in the enclosing object, here it's query and object.
  • arg: An argument for the enclosing field.
  • resolve: Resolve function for the enclosing field.

We are also defining an object type :item using another two built-in scalar types :id represents a unique number and :string is obvious.

And we are using the type :item for the query:menu_item to return a map with this type.

Add this in MenuCardWeb.Router to access GraphiQL interface provided by Absinthe.

# lib/menu_card_web/router.ex

defmodule MenuCardWeb.Router do
  ...
  forward "/graphiql", Absinthe.Plug.GraphiQL, schema: MenuCardWeb.Schema
end

Go to localhost:4000/graphiql and run a query:

{
  menuItem(id: "bar") {
    name
  }
}

result:

{
  "data": {
    "menuItem": {
      "name": "Burger"
    }
  }
}

With Ecto:

Let's use mix tasks to generate contexts, schemas, and migrations for items and their reviews.

$ mix phx.gen.context Menu Item items name:string price:integer
 
$ mix phx.gen.context Menu Review reviews comment:string author_id:integer item_id:references:items

These will create our required migrations and columns. This should have added lib/menu_card/menu/item.ex, lib/menu_card/menu/review.ex, lib/menu_card/menu.ex and migrations inside prev/repo/migrations.

Edit review.ex to let us add item_id when creating a review.

# lib/menu_card/menu/review.ex

defmodule MenuCard.Menu.Review do
  use Ecto.Schema
  import Ecto.Changeset

  schema "reviews" do
    field(:comment, :string)
    field(:author_id, :integer)

    belongs_to(:item, MenuCard.Menu.Item)

    timestamps()
  end

  @doc false
  def changeset(review, attrs) do
    review
    |> cast(attrs, [:comment, :author_id, :item_id])
    |> validate_required([:comment, :author_id, :item_id])
  end
end

Run the migrations:

$ mix ecto.migrate

Create and Get an item

Let's write a mutation, and types we need to create an item in the schema. Delete old code in schema and start with empty file:

# lib/menu_card_web/schema.ex

defmodule MenuCardWeb.Schema do
  use Absinthe.Schema

  @desc "An item"
  object :item do
    field(:id, :id)
    field(:name, :string)
    field(:price, :integer)
    field(:reviews, list_of(:review))
  end

  @desc "Review for an item"
  object :review do
    field(:id, :id)
    field(:comment, :string)
    field(:author_id, :integer)
  end

  mutation do
    field :create_item, :item do
      arg(:name, non_null(:string))
      arg(:price, non_null(:integer))

      resolve(fn args, _ ->
        {:ok, MenuCard.Menu.create_item(args)}
      end)
    end
  end
end

Here, the only difference is the language(Elixir), and the rest of GraphQL spec remains unchanged from the previous blog.

Few points I'd like to add are: how the resolve functions and constraints on field type differs in absinthe then the NodeJS version.

  • Resolver functions can be a 3 or 2 arity function.

    • 3 arity resolver:

      item(id: 1){
        name
      }

      The first argument will be the parent, i.e, the resolved values of item(id: 1) will be the parent of the field name.

      The second argument will be args passed for the field, so, for the field item(id: 1) the args will be %{id: 1}

      The third argument will be the global context that we can set as a plug.

    • 2 arity resolver: Here, the first argument will be args and second will be context.

  • list_of(object_type/sclar-type): Returned value or arg passed should be a list. Equalent to [TypeName]

  • non_null(object_type/scalar-type): Returned value or arg should be passed i.e, not null. Equalent to TypeName!

  • Resolver function should return a tuple with first element as :ok or :error and second the element should be a map.

Now, if you run

$ iex -S mix phx.server

You will see an error saying there should be a query object, which is the root of all objects we define. A mutation is also a rootMutation object but mutation object is allowed to be null.

Let's add a query and try:

Add this before mutation object:

query do
  field :item, :item do
    arg(:id, non_null(:id))

    resolve(fn args, _ ->
      {:ok, MenuCard.Menu.get_item(args)}
    end)
  end
end

Note: There is also another type of object which is subscriptions. We'll see it extensively in another chapter.

Now run iex -S mix phx.server and open localhost:4000/graphiql to use GraphIQL interface. In the left text area, write the query and hit the play button or ctrl + enter to run the query.

Query:

mutation {
  createItem(name: "Rice pudding", price: 30) {
    id
    name
    price
  }
}

Result:

{
  "data": {
    "createItem": {
      "id": "1",
      "name": "Rice pudding"
    }
  }
}

Hurray 🎉!

We finished the basic query and mutation.

Loading associations

You can see that we have reviews for each item

@desc "An item"
object :item do
  field(:id, :id)
  field(:name, :string)
  field(:price, :integer)
  field(:reviews, list_of(review))
end

We can load review association in three ways

  • Write a separate resolver for it

    object :item do
      field(:id, :id)
      field(:name, :string)
      field(:price, :integer)
    
      field(:reviews, list_of(:review)) do
        resolve(fn parent, _, _ ->
          {:ok, MenuCard.Menu.get_reviews_by_item(parent.id)}
        end)
      end
    end
  • Return reviews in the :item resolver itself.

    # lib/menu_card/menu.ex
    
    defmodule MenuCard.Menu do
      ...
    
      def get_item(%{id: id}) do
        Repo.get!(Item, id)
        |> Repo.preload(:reviews)
      end
    
    end
  • Absinthe recommends batching using dataloader to load association.

Even through we'll stick with using preload. This is not a best practice and comes with cons, try to use dataloader for prod.

Add the function which uses preload to your code.

To get reviews with items, first, we need a way to create them: mutations

Add a mutation in schema:

# lib/menu_card_web/schema.ex

defmodule MenuCardWeb.Schema do

  ...

  mutation do

    ...

    field :do_review, :review do
      arg(:comment, non_null(:string))
      arg(:author_id, non_null(:id))
      arg(:item_id, non_null(:id))

      resolve(fn args, _ ->
        {:ok, MenuCard.Menu.create_review(args)}
      end)
    end
  end
end

Reset and start with fresh DB:

$ mix ecto.reset
$ iex -S mix phx.server

We will create a Menu Item and then add a Review for it

mutation {
  createItem(name: "Rice pudding", price: 40) {
    name
    price
  }
}

Result:

{
  "data": {
    "createItem": {
      "name": "Rice pudding",
      "price": 40
    }
  }
}

With that returned id, create a review:

mutation {
  doReview(itemId: 1, authurId: 1, comment: "Yummmmy!") {
    comment
  }
}

Result:

{
  "data": {
    "doReview": {
      "comment": "asdad"
    }
  }
}

That's it, that's how you would create an HTTP GraphQL API with phoenix framework.

Here is the code.

If you want to do more with this, create a mutation to delete and edit both items and review.

See you in the next post: How to utilize these API in the front-end using apollo-client with ReactJS

Good luck learning! 😇