Tutorial
Getting started with GraphQL and Absinthe in Phoenix
This post was updated 04 Feb - 2022
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.