Tutorial

Getting started with GraphQL and Absinthe in Phoenix

This post was updated 04 Feb - 2022

absinthe graphql api phoenix

In the last tutorial, there I had an app with a simple rest api that was authenticated with Guardian and Json Web Token. In this tutorial, I will go into also adding the same functionality in Phoenix in GraphQl with the Absinthe library.

STEP 1 - Installation

Add Absinthe and Absinthe Plug to the mix file:

# mix.exs
defp deps do
  [
    {:absinthe, "~> 1.5.0"},
    {:absinthe_plug, "~> 1.5.0"},
  ]
end
And run 
mix deps.get

Next is to add routes for both the graphql schema and Graphiql, the visual query helper and documentation tool for the api.

# lib/graphql_tutorial_web/router.ex
defmodule GraphqlTutorialWeb.Router do
  ...rest of routes  pipeline :graphql do
    # Will be used later
  end  scope "/api" do
    pipe_through :graphql    forward "/", Absinthe.Plug, schema: GraphqlTutorialWeb.Schema
  end  if Mix.env == :dev do
    forward "/graphiql", Absinthe.Plug.GraphiQL, schema: GraphqlTutorialWeb.Schema
  end
end

Note that the Absinthe Plug we are adding are pointing to a Schema file that I also need to create before I can start the server.

Also note that the Graphiql interface are only accessible in dev.

STEP 2 - Getting to the first query

As I mentioned above, we already have an existing json endpoint and a database storage for products. So what I want to do is to just hook up with that.

Remember that in the router, I specified a specific schema. It will be a single schema file where I build the entire api.

Create the file:

# lib/graphql_tutorial_web/schema.ex
defmodule GraphqlTutorialWeb.Schema do
  use Absinthe.Schema  alias GraphqlTutorialWeb.Schema  import_types(Schema.ProductTypes)  query do
    import_fields(:get_products)
  end
end

Note that I import ProductTypes. That is the module that will handle specify and handle calling the product part of the api. I need to add the file:

# lib/graphql_tutorial_web/schema/product_types.ex
defmodule GraphqlTutorialWeb.Schema.ProductTypes do
  use Absinthe.Schema.Notation  alias GraphqlTutorialWeb.Resolvers  @desc "A product"
  object :product do
    field :id, :id
    field :name, :string
    field :description, :string
    field :price, :float
  end  object :get_products do
    @desc """
    Get a list of products
    """    field :products, list_of(:product) do
      resolve(&Resolvers.Products.list_products/2)
    end
  end
end

Note that in the code above, I refer to Resolvers.Products.list_products. Resolvers will be the modules that connects to the products context. The products resolver looks like this:

# lib/graphql_tutorial_web/resolvers/products.ex
defmodule GraphqlTutorialWeb.Resolvers.Products do
  alias GraphqlTutorial.Products  def list_products(_args, _context) do
    {:ok, Products.list_products()}
  end
end

That should be enough to have something going. I can start the server and go to the visual interface:

http://localhost:4000/graphiql

In the query interface, i can in the bottom left write:

{
  products {
    id
    name
    price
    description
  }
}

And run the query. If everything works, I should get a result:

STEP 3 - User creation and authentication

Since I already have guardian installed in the app I will just utilize that. However, I need to create a specific plug that works with Absinthe. What it does, it that it can take a resource from the json web token and add it to Absinthes "context". That is useful to see if a user is authorized. The plug looks like:

# lib/graphql_tutorial_web/plugs/context.ex
defmodule GraphqlTutorialWeb.Context do
  @behaviour Plug  import Plug.Conn  def init(opts), do: opts  def call(conn, _) do
    case build_context(conn) do
      {:ok, context} ->
        put_private(conn, :absinthe, %{context: context})      _ ->
        conn
    end
  end  defp build_context(conn) do
    with ["" <> token] <- get_req_header(conn, "authorization"),
         {:ok, user, _claims} <- GraphqlTutorial.Guardian.resource_from_token(token) do
      {:ok, %{current_user: user}}
    end
  end
end

I add in in the router in the new graphql pipeline that I already set up.

# lib/graphql_tutorial_web/router.ex
defmodule GraphqlTutorialWeb.Router do
  ...rest of routes  pipeline :graphql do
    plug GraphqlTutorialWeb.Context
  end..rest of routesend

I already made a producttypes so now I also need the usertypes. I want a user to both be able to register and login.

# lib/graphql_tutorial_web/schema/user_types.ex
defmodule GraphqlTutorialWeb.Schema.UserTypes do
  use Absinthe.Schema.Notation  alias GraphqlTutorialWeb.Resolvers  @desc "A user"
  object :user do
    field :email, :string
    field :id, :id
  end  object :create<i>user</i>mutation do
    @desc """
    create user
    """    @desc "Create a user"
    field :create_user, :user do
      arg(:email, non_null(:string))
      arg(:password, non_null(:string))      resolve(&Resolvers.Accounts.create_user/3)
    end
  end  object :login_mutation do
    @desc """
    login with the params
    """    field :create_session, :session do
      arg(:email, non_null(:string))
      arg(:password, non_null(:string))
      resolve(&Resolvers.Accounts.login/2)
    end
  end  @desc "session value"
  object :session do
    field(:token, :string)
    field(:user, :user)
  end
end

And the resolver for the user login and creation.

# lib/graphql_tutorial_web/resolvers/accounts.ex
defmodule GraphqlTutorialWeb.Resolvers.Accounts do
  alias GraphqlTutorial.Accounts
  alias GraphqlTutorial.Accounts.User  def create<i>user(</i>parent, args, _context) do
    Accounts.register_user(args)
  end  def login(%{email: email, password: password}, _info) do
    with %User{} = user <- Accounts.get_user_by_email_and_password(email, password),
         {:ok, jwt, _full_claims} <- GraphqlTutorial.Guardian.encode_and_sign(user) do
      {:ok, %{token: jwt}}
    else
      _ -> {:error, "Incorrect email or password"}
    end
  end
end

Note that I call the resolver Accounts. That is because I started the app with Phx Gen Auth and that generator created an Accounts context for handling user signups and logins. I call the resolver the same so they map up better.

I also need to import the user_types in the schema. In Graphql world, whenever you to a create or update, that is called mutations. So I will add that in a mutations block:

# lib/graphql_tutorial_web/schema.ex
defmodule GraphqlTutorialWeb.Schema do
  use Absinthe.Schema  alias GraphqlTutorialWeb.Schema  import_types(Schema.UserTypes)
  import_types(Schema.ProductTypes)  query do
    import<i>fields(:get</i>products)
  end  mutation do
    import_fields(:login_mutation)
    import_fields(:create_user_mutation)
  end
end

Now I have enough for testing the user login with my existing user (setup in previous tutorial). I will once again go the the graphical interface.

First I will start with wrong credentials:

And get the error message. But with the correct credentials:

So far, so good. Now when I now I can get a valid json web token, I can move forward.

STEP 4 - Adding the products CRUD

Now its time to add the products CRUD in GraphQl and I want to authenticate the mutations with Guardian and JsonWebToken. So, to be clear, I want anybody to be able to see/query for the products, but only signed in users to be able to create, update and delete. In this step, I will focus on creating.

I need to extend the products resolver with a create_product function:

# lib/graphql_tutorial_web/resolvers/products.ex
defmodule GraphqlTutorialWeb.Resolvers.Products do
  alias GraphqlTutorial.Products
  alias GraphqlTutorial.Products.Product  def list<i>products(</i>args, _context) do
    {:ok, Products.list_products()}
  end  def create<i>product(args, %{context: %{current</i>user: _user}}) do
    case Products.create_product(args) do
      {:ok, %Product{} = product} -> {:ok, product}
      {:error, changeset} -> {:error, inspect(changeset.errors)}
    end
  end  def create_product(_args, _context), do: {:error, "Not Authorized"}
end

Note the pattern matching going on for figuring out if a user is authenticated. The context part comes from the Context plug I added earlier. The idea is that if there is no %{current_user: _user} then the user is not authenticated.

In the schema, I also need to add the mutation

# lib/graphql_tutorial_web/schema.ex
defmodule GraphqlTutorialWeb.Schema do
 # ... rest of the code
  mutation do
    import_fields(:login_mutation)
    import_fields(:create_user_mutation)
		
    import_fields(:create_product)
  end
end

Now I can go to the query interface and try to create a product:


mutation (
  $name: String!,
  $description: String!,
  $price: Float!,
) {
  product: createProduct(
    name: $name,
    description: $description,
    price: $price
  ) {
    id
    name
    description
    price
  }
}// Varaibles
{
  "name": "Awesome Product",
  "description": "Some longer desription",
  "price": 200.50
}

However, without providing a valid json web token, I get an error/unauthorized message

So, what I need to do is to once again do a create user mutation.

mutation($email: String!, $password: String!) {
  session: createSession(email: $email, password: $password) {
  	token
  }
}
// variables
{
  "email": "andreas@example.com",
  "password": "supersecret123"
}

And with the token I get back, I can add it as a request header:

Note that I need to add the "bearer: " before the token.

Now, everything should be good and I can see that the product was created.

STEP 5 - Full product crud

For the full products CRUD I need to add a query for getting a single product by id, update a product and delete a product.

The full schema file looks like:

defmodule GraphqlTutorialWeb.Schema do
  use Absinthe.Schema  alias GraphqlTutorialWeb.Schema  import_types(Schema.UserTypes)
  import_types(Schema.ProductTypes)  query do
    import<i>fields(:get</i>products)
    import<i>fields(:get</i>product)
  end  mutation do
    import<i>fields(:login</i>mutation)
    import<i>fields(:create</i>user_mutation)    import_fields(:create_product)
    import_fields(:update_product)
    import_fields(:delete_product)
  end
end

I also need to add the extended logic in the ProductTypes schema module:

defmodule GraphqlTutorialWeb.Schema.ProductTypes do
  use Absinthe.Schema.Notation  alias GraphqlTutorialWeb.Resolvers  @desc "A product"
  object :product do
    field :id, :id
    field :name, :string
    field :description, :string
    field :price, :float
  end  object :get_products do
    @desc """
    Get a list of products
    """    field :products, list_of(:product) do
      resolve(&Resolvers.Products.list_products/2)
    end
  end  object :get_product do
    @desc """
    Get a specific product
    """    field :product, :product do
      arg(:id, non_null(:id))      resolve(&Resolvers.Products.get_product/2)
    end
  end  object :create_product do
    @desc """
    Create a product
    """    @desc "Create a product"
    field :create_product, :product do
      arg :name, non_null(:string)
      arg :description, non_null(:string)
      arg :price, non_null(:float)      resolve(&Resolvers.Products.create_product/2)
    end
  end  object :update_product do
    @desc """
    Update a product
    """    @desc "Update a product"
    field :update_product, :product do
      arg(:id, non_null(:id))
      arg(:name, :string)      resolve(&Resolvers.Products.update_product/2)
    end
  end  object :delete_product do
    @desc """
    Delete a specific product
    """    field :delete_product, :product do
      arg(:id, non_null(:id))      resolve(&Resolvers.Products.delete_product/2)
    end
  end
end

Finally, I need to add more functions to the products resolver. The final resolver will look like:

defmodule GraphqlTutorialWeb.Resolvers.Products do
  alias GraphqlTutorial.Products
  alias GraphqlTutorial.Products.Product  def list<i>products(</i>args, _context) do
    {:ok, Products.list_products()}
  end  def get<i>product(%{id: id}, </i>context) do
    {:ok, Products.get_product(id)}
  end  def create<i>product(args, %{context: %{current</i>user: _user}}) do
    case Products.create_product(args) do
      {:ok, %Product{} = product} -> {:ok, product}
      {:error, changeset} -> {:error, inspect(changeset.errors)}
    end
  end  def create<i>product(</i>args, _context), do: {:error, "Not Authorized"}  def update<i>product(%{id: id} = params, %{context: %{current</i>user: _user}}) do
    case Products.get_product(id) do
      nil ->
        {:error, "Product not found"}      %Product{} = product ->
        case Products.update_product(product, params) do
          {:ok, %Product{} = product} -> {:ok, product}
          {:error, changeset} -> {:error, inspect(changeset.errors)}
        end
    end
  end  def update<i>product(</i>args, _context), do: {:error, "Not Authorized"}  def delete<i>product(%{id: id}, %{context: %{current</i>user: _user}}) do
    case Products.get_product(id) do
      nil -> {:error, "Product not found"}
      %Product{} = product -> Products.delete_product(product)
    end
  end  def delete_product(_args, _context), do: {:error, "Not Authorized"}
end

Conclusion

Even though the code seems quite verbose, you need to keep in mind that its self documenting. The Graphiql will tell an api consumer what fields are available, required, type and so one.

In the next tutorial, I am planning to go through testing. I basically test this with request tests. But it requires a little setup with Guardian.

Related Tutorials

Published 04 May - 2021
Updated 05 May - 2022

How to combine Phoenix LiveView with Alpine.js

No matter how great Phoenix LiveView is, there is still some use case for sprinking some JS in your app to improve UX. For example, tabs, dropdowns,..

Published 26 Jan - 2020
Updated 01 May - 2020

Create Swagger compatible custom Phoenix JSON generator

I am in the process of creating an API in Phoenix and I want it to support Swagger documentation without me having to do much. I dont want to go in ..