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:
Assuming the installation is complete, let's go to our project setup.
Project setup
Create a new Phoenix project:
Create the database and start the server:
Phoenix app template ships with postgres as default database adapter with
postgres
user andpostgres
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:
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
andobject
. - 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:
result:
With Ecto:
Let's use mix tasks to generate contexts, schemas, and migrations for items and their reviews.
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:
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:
The first argument will be the parent, i.e, the resolved values of
item(id: 1)
will be the parent of the fieldname
.The second argument will be
args
passed for the field, so, for the fielditem(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 becontext
.
-
-
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
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:
Result:
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:
We will create a Menu Item and then add a Review for it
Result:
With that returned id, create a review:
Result:
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! 😇