GraphQL With Elixir Phoenix And Absinthe
2021-04-29
Elixir is a language that lends itself perfectly to creating graphql apis and the creators of Absinthe have written a great implementation of the graphql specification. We will create a very simple graphql api with queries, mutations and subscriptions. Then we will take it for a ride in graphiql. A graphql playground that lets you test out your api. I won’t explain what graphql is, I’ll just show you how to do it the Absinthe way.
We are going to create an app that tracks the status of workers at a call center. You can imagine you want to know who is signed in and if the are in a call or not. Our graphql api could be used to update a realtime dashboard.
First create a new Phoenix project:
mix phx.new employee_status --no-webpack --no-html
Then add these lines to your mix.exs:
{:absinthe, "~> 1.4"},
{:absinthe_plug, "~> 1.4"},
{:absinthe_phoenix, "~> 2.0"},
{:poison, "~> 2.1.0"}
Then run:
mix deps.get
Now we have to generate the context for our app:
mix phx.gen.context Staff Employee employees name:string status:string
Very simple we just have employee names and their status. Lets get ecto setup:
mix ecto.setup
Now we will start working on our graphql schema so lets make a new file at lib/employee_status_web/schema.ex:
defmodule EmployeeStatusWeb.Schema do
use Absinthe.Schema
alias EmployeeStatusWeb.EmployeeResolver
object :employee do
field :id, non_null(:id)
field :name, non_null(:string)
field :status, non_null(:string)
end
query do
@desc "Get all employees"
field :all_employees, non_null(list_of(non_null(:employee))) do
resolve(&EmployeeResolver.all_employees/3)
end
end
end
We have our base object which is very similar to the ecto schema we generated before. We also have our first query. It is being resolved by the EmployeeResolver module. Lets fill that out next. Make a new file at lib/employee_status_web/resolvers/employee_resolver.ex:
defmodule EmployeeStatusWeb.EmployeeResolver do
alias EmployeeStatus.Staff
def all_employees(_root, _args, _info) do
{:ok, Staff.list_employees()}
end
end
Ok, simple enough we are just using the CRUD operations we generated earlier. Speaking of CRUD lets just do them all the graphql way. Lets add some mutations to our schema for creating, updating and deleting an employee. Open up the schema again and add these lines:
mutation do
@desc "Create a new employee"
field :create_employee, :employee do
arg :name, non_null(:string)
arg :status, non_null(:string)
resolve &EmployeeResolver.create_employee/3
end
@desc "Delete an employee"
field :delete_employee, :employee do
arg :id, non_null(:id)
resolve &EmployeeResolver.delete_employee/3
end
@desc "Update an employee status"
field :update_employee_status, :employee do
arg :id, non_null(:id)
arg :status, non_null(:string)
resolve &EmployeeResolver.update_employee_status/3
end
end
Also add these resolvers the EmployeeResolvers module:
def create_employee(_root, args, _info) do
case Staff.create_employee(args) do
{:ok, employee} ->
{:ok, employee}
_error ->
{:error, "could not create employee"}
end
end
def delete_employee(_root, %{id: id}, _info) do
employee = Staff.get_employee!(id)
case Staff.delete_employee(employee) do
{:ok, employee} ->
{:ok, employee}
_error ->
{:error, "could not delete employee"}
end
end
def update_employee_status(_root, %{id: id, status: status}, _info) do
employee = Staff.get_employee!(id)
case Staff.update_employee(employee,%{status: status}) do
{:ok, employee} ->
{:ok, employee}
_error ->
{:error, "could not update employee"}
end
end
Now we are almost ready to test out our graphql endpoint. Fist add the following to your router:
scope "/" do
pipe_through :api
forward "/graphiql", Absinthe.Plug.GraphiQL,
schema: EmployeeStatusWeb.Schema,
interface: :simple,
context: %{pubsub: EmployeeStatusWeb.Endpoint}
end
This sets up the graphql playground so we can test our api. Run:
mix phx.server
and point your browswer at http://localhost:4000/graphiql. You should be able to run all the queries and mutations that we set up! Great, now lets try some subscriptions. Lets say we want our dashboard to be notified anytime a user status is updated so we can display that. Lets set up a subscription so that anytime some other client run the update_employee_status mutation our subscription will receive the data of that employee with the updated status. Lets go back to our Schema module and add this:
subscription do
@desc "Subscribe to any employee status change"
field :updated_any_employee_status, :employee do
config fn _, _ ->
{:ok, topic: :any_employee_updated}
end
trigger :update_employee_status, topic: fn _ ->
:any_employee_updated
end
end
end
Also we need to configure pubsub to use Absinthe since that is how subscriptions will be broadcast out. To achieve this we will editing many files. first add this to lib/employee_status_web/channels/user_socket.ex:
defmodule EmployeeStatusWeb.UserSocket do
use Phoenix.Socket
# add this line
use Absinthe.Phoenix.Socket, schema: EmployeeStatusWeb.Schema
Now lets add to lib/employee_status_web/endpoint.ex:
defmodule EmployeeStatusWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :employee_status
# Add this line
use Absinthe.Phoenix.Endpoint
Ok, now lets add to lib/employee_status_web/router.ex:
scope "/" do
pipe_through :api
forward "/graphiql", Absinthe.Plug.GraphiQL,
schema: EmployeeStatusWeb.Schema,
interface: :simple,
context: %{pubsub: EmployeeStatusWeb.Endpoint},
# Add this line
socket: EmployeeStatusWeb.UserSocket
end
Finally go to lib/employee_status/application.ex and add this line:
def start(_type, _args) do
children = [
EmployeeStatus.Repo,
EmployeeStatusWeb.Telemetry,
{Phoenix.PubSub, name: EmployeeStatus.PubSub},
EmployeeStatusWeb.Endpoint,
# Add this line
{Absinthe.Subscription, EmployeeStatusWeb.Endpoint}
]
We have set up subscriptions! We can now restart our server and go back to graphiql and test out our api. I hope this can be a good jumping off point for implementing more complex systems.