Psst. It would be super cool if you could try the new Phoenix Boilerplate

Try now →

Tutorial

Getting started with GraphQL and Absinthe in Phoenix

phoenixapiabsinthegraphql

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 routes

end

I already made a product_types so now I also need the user_types. 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_user_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_user(_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_fields(:get_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_products(_args, _context) do
    {:ok, Products.list_products()}
  end

  def create_product(args, %{context: %{current_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_fields(:get_products)
    import_fields(:get_product)
  end

  mutation do
    import_fields(:login_mutation)
    import_fields(:create_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_products(_args, _context) do
    {:ok, Products.list_products()}
  end

  def get_product(%{id: id}, _context) do
    {:ok, Products.get_product(id)}
  end

  def create_product(args, %{context: %{current_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"}

  def update_product(%{id: id} = params, %{context: %{current_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_product(_args, _context), do: {:error, "Not Authorized"}

  def delete_product(%{id: id}, %{context: %{current_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 26 Jan

Create Swagger compatible custom Phoenix JSON generator

apiphoenixswagger

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 ..

Published 22 Jul

Combining authentication solutions with Guardian and Phx Gen Auth

authenticationguardianapi

Many web apps have both a web interface and an Json api. When the normal web app has a classic session based authentication, an API need something l..