GraphQL enables consumers of an API to ask for the exact data they want from it. This is as opposed to REST, where the API provider dictates what and how data will be served, and it is up to the consumer to make sense of whatever data it receives.
In an app that uses Phoenix for the back end and Elm for the front end, the flow of data via APIs for a query will usually take the form of:
- Elm requests Phoenix for data via an API call
- Phoenix provides the requested data via a JSON response
- Elm decodes the data from the response and displays it
This blog post will cover migrating the APIs of an existing Phoenix/Elm from using REST to using GraphQL, including:
- Adding GraphQL schemas and types to the Phoenix back end
- Migrating from Phoenix controllers to resolvers
- Migrating Elm-side JSON response decoding from using
JSON.Decode
toValueSpec
from theelm-graphql
Elm package. - Translating GraphQL requests created in GraphiQL into Elm code, and sending them to Phoenix
Starting Point
The app that we are going to use as a baseline to migrate from REST to GraphQL is an Address Book app that was originally created by Ricardo García Vega over a series of blog posts (Ricardo’s Github repo).
I learned a lot from coding up the app while reading those posts, and I thank Ricardo sincerely for putting the time into his write-ups! Afterwards, I upgraded the app to Phoenix 1.3, played around with the codebase, and put my version of it in its own Github repository, so it is this version of the app that we will use. If you are following along at home, please clone my repo and follow the README
instructions to get up and running, or you can skip straight to the finished product, which is in the repo’s graphql
branch.
Current State of Play
Before jumping into migrating to GraphQL, let’s take a look at some of the application’s current structure and see how communication is done via REST requests.
Back End
First, let’s have a look at the router:
lib/phoenix_and_elm_web/router.ex
defmodule PhoenixAndElmWeb.Router do
# ...
scope "/api", PhoenixAndElmWeb do
pipe_through :api
scope "/v1", V1 do
resources "/contacts", ContactController, only: [:index, :show]
end
end
scope "/", PhoenixAndElmWeb do
pipe_through :browser
get "/*path", AddressBookController, :index
end
end
In this app, AddressBook
is the Phoenix context behind which Contact
s live. So, the AddressBookController
’s purpose is solely to render the HTML tag where the Elm app will be embedded:
lib/phoenix_and_elm_web/templates/address_book/index.html.eex
<div id="elm-main"></div>
Once the Elm app is embedded, it can make calls out to the versioned (/v1
) contact APIs to fetch contact information, which will be handled by the ContactController
:
lib/phoenix_and_elm_web/controllers/v1/contact_controller.ex
defmodule PhoenixAndElmWeb.V1.ContactController do
use PhoenixAndElmWeb, :controller
alias PhoenixAndElm.AddressBook
def index(conn, params) do
contacts = AddressBook.list_contacts(params)
json(conn, contacts)
end
def show(conn, %{"id" => id}) do
contact = AddressBook.get_contact!(id)
json(conn, contact)
end
end
There are two APIs that Phoenix provides: listing contacts and showing (retrieving) information for a contact. Each function talks only to the AddressBook
context, leaving the responsibility of determining how the information requested is provided up to the Address Book “sub-system”: as far as the controller functions are concerned, they provide some parameter to an AddressBook
function, and get returned some value which they then serialize into JSON. So, this controller is effectively our REST boundary, and it is these few lines of functionality that will need to be replicated when migrating over to GraphQL.
Front End
The contact API URL (api/v1/contacts
), that maps to the ContactController
in the Phoenix app, is defined in a common Commands.elm
file so that it can be easily shared between the different API calls coming from Elm:
assets/elm/src/Commands.elm
module Commands exposing (contactsApiUrl)
contactsApiUrl : String
contactsApiUrl =
"/api/v1/contacts"
The two main models in the Elm app are Contact
and ContactList
, and code related to how to fetch information to fill the records of these models is kept in Commands
files under directories named after the model itself.
Contact via REST
Let’s see how information for a single contact is retrieved:
assets/elm/src/Contact/Commands.elm
module Contact.Commands exposing (fetchContact)
import Commands exposing (contactsApiUrl)
import Contact.Decoder as Decoder
import Contact.Messages exposing (ContactMsg(FetchContact))
import Http
import Messages exposing (Msg(ContactMsg))
fetchContact : Int -> Cmd Msg
fetchContact id =
let
apiUrl =
contactsApiUrl ++ "/" ++ toString id
in
Decoder.decoder
|> Http.get apiUrl
|> Http.send FetchContact
|> Cmd.map ContactMsg
When Contact.Commands.fetchContact
is called within the Elm app, a Cmd
is sent to the Elm Runtime, with a Msg
type of ContactMsg FetchContact
, telling it to send a request to the apiUrl
(looking something like api/v1/contacts/5
), and decode the response using Contact.Decoder
.
The decoder used for a Contact
looks like the following:
assets/elm/src/Contact/Decoder.elm
module Contact.Decoder exposing (decoder)
import Contact.Model exposing (Contact)
import Json.Decode as Decode exposing (field, int, string)
import Json.Decode.Extra exposing ((|:))
decoder : Decode.Decoder Contact
decoder =
Decode.succeed
Contact
|: (field "id" int)
|: (field "first_name" string)
|: (field "last_name" string)
|: (field "gender" int)
|: (field "birth_date" string)
|: (field "location" string)
|: (field "phone_number" string)
|: (field "email" string)
|: (field "headline" string)
|: (field "picture" string)
The ContactMsg FetchContact
message gets handled in Contact.Update
, which updates the Contact
model record:
assets/elm/src/Contact/Update.elm
module Contact.Update exposing (update)
import Contact.Messages exposing (ContactMsg(FetchContact))
import Messages exposing (Msg)
import Model exposing (Model, RemoteData(Failure, Success))
update : ContactMsg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
FetchContact (Ok response) ->
( { model | contact = Success response }, Cmd.none )
FetchContact (Err error) ->
( { model | contact = Failure "Contact not found" }, Cmd.none )
The use of Cmd.map ContactMsg
in Contact.Commands
is what enables the FetchContact
message to be handled in a “child” update
function (in this case Contact.Update
is considered a child of Update
), which can help reduce the size of the “parent” update
function:
module Update exposing (update, urlUpdate)
import Contact.Update
import Messages exposing (Msg(ContactMsg, ContactListMsg, NavigateTo, ...))
import Model exposing (Model, RemoteData(NotRequested, Requesting))
-- ...
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
ContactMsg msg ->
Contact.Update.update msg model
ContactListMsg msg ->
ContactList.Update.update msg model
UpdateSearchQuery value ->
( { model | search = value }, Cmd.none )
-- ...
-- ...
Here, in the “parent” update
function, there are messages that are handled directly, like UpdateSearchQuery
, while messages that are wrapped in a ContactMsg
message, for example, are delegated straight off to the Contact.Update.update
function.
See this blog post about “The Translator Pattern” in Elm for more information about this style of message passing.
Contact List via REST
Now that we know about fetching a single contact to populate a Contact
record, what about fetching a list of contacts to populate a ContactList
record?
assets/elm/src/ContactList/Commands.elm
module ContactList.Commands exposing (fetchContactList)
import Commands exposing (contactsApiUrl)
import ContactList.Messages exposing (ContactListMsg(FetchContactList))
import ContactList.Decoder as Decoder
import Http
import Messages exposing (Msg(ContactListMsg))
fetchContactList : Int -> String -> Cmd Msg
fetchContactList page search =
let
apiUrl =
contactsApiUrl
++ "?search="
++ search
++ "&page="
++ (toString page)
in
Decoder.decoder
|> Http.get apiUrl
|> Http.send FetchContactList
|> Cmd.map ContactListMsg
Fetching a list of contacts looks very similar to fetching a single contact from an API callout point of view, except that we are now providing page
and search
parameters, resulting in an apiUrl
that looks something like api/v1/contacts?search=paul&page=2
.
Just like with the contact API call, we decode the response, this time using the ContactList.Decoder
, and then send a Cmd
with a Msg
of type ContactListMsg FetchContactList
, which will then be handled in the ContactList.Update.update
function.
The ContactList.Decoder
itself delegates its Contact
-decoding logic to Contact.Decoder
, defining only fields related to data about a paginated list of records:
assets/elm/src/ContactList/Decoder.elm
module ContactList.Decoder exposing (decoder)
import Contact.Decoder
import ContactList.Model exposing (ContactList)
import Json.Decode as Decode exposing (field, int, list)
import Json.Decode.Extra exposing ((|:))
decoder : Decode.Decoder ContactList
decoder =
let
contact =
Contact.Decoder.decoder
in
Decode.succeed
ContactList
|: (field "entries" (list contact))
|: (field "page_number" int)
|: (field "total_entries" int)
|: (field "total_pages" int)
The reason that the field
for a list of contacts is specifically named "entries"
is due to the Phoenix app using Scrivener.Ecto for pagination. Paginated contact lists are provided in a Scrivener.Page
struct, that contains the list of items its paginating under an :entries
map key.
And I think that about covers the request/response handling code on the front end. So, it looks like we will need to:
- Change the URL in
Commands.elm
to reference a different GraphQL endpoint URL - Switch out the
Http
package for a GraphQL client package - Remove decoders, and replace with GraphQL request builders
Migrate Back End to GraphQL
Phew, that is a fair bit to take in for what is a tour of only one conceptual part of a toy app, but at least now we have an idea of the API-related parts of the app that are targets for change in both the front and back ends.
So, without further ado, let’s tackle migration of the back end first.
Absinthe
Absinthe is the go-to toolkit for using GraphQL in Elixir, with its authors pretty much writing the book on the subject, hence we will be use it in this project. So, open up mix.exs
, and add the following libraries:
mix.exs
defmodule PhoenixAndElm.Mixfile do
# ...
defp deps do
[
# ...
{:absinthe, "~> 1.4"},
{:absinthe_plug, "~> 1.4"}
]
end
end
Note that even though this is a Phoenix app, for this example we will not need the Absinthe.Phoenix package since we will be sending messages via HTTP, and not via Phoenix channels/websockets.
Types
Once you have run mix deps.get
, create a new lib/phoenix_and_elm_web/schema/
directory and let’s create some GraphQL types to describe the data we want to query:
lib/phoenix_and_elm_web/schema/types.ex
defmodule PhoenixAndElmWeb.Schema.Types do
use Absinthe.Schema.Notation
import_types(Absinthe.Type.Custom)
object :contact_list do
field(:total_entries, :integer)
field(:total_pages, :integer)
field(:page_number, :integer)
field(:page_size, :integer)
field(:entries, list_of(:contact))
end
object :contact do
field(:id, :integer)
field(:first_name, :string)
field(:last_name, :string)
field(:gender, :integer)
field(:birth_date, :date)
field(:location, :string)
field(:phone_number, :string)
field(:email, :string)
field(:headline, :string)
field(:picture, :string)
end
end
The :contact
object
almost directly mirrors the contact database schema, with the one small caveat here being that the GraphQL specification does not provide a :date
type for the :birth_date
field, so the Absinthe.Type.Custom
module provides one that we can use.
The :contact_list
object
essentially describes a Scrivener.Page
, though for simplicity’s sake, we are limiting entries
to only containing a list_of(:contact)
(Scrivener can, of course, paginate other types of things!).
Schema
Now, we need the schema itself to describe the queries we will allow into the Phoenix app, what arguments each query takes, and what execution should happen for each valid query (done here in the form of resolvers).
lib/phoenix_and_elm_web/schema/schema.ex
defmodule PhoenixAndElmWeb.Schema do
use Absinthe.Schema
alias PhoenixAndElmWeb.ContactResolver
import_types(PhoenixAndElmWeb.Schema.Types)
query do
field :contacts, type: :contact_list do
arg(:search, non_null(:string))
arg(:page, non_null(:integer))
resolve(&ContactResolver.list_contacts/3)
end
field :contact, type: :contact do
arg(:id, non_null(:id))
resolve(&ContactResolver.get_contact/3)
end
end
end
Resolvers
Resolvers can tend to get quite long, so it is considered good practice to put them into their own top level directory under the web app, so let’s do that and create a ContactResolver
:
lib/phoenix_and_elm_web/resolvers/contact_resolver.ex
defmodule PhoenixAndElmWeb.ContactResolver do
alias PhoenixAndElm.AddressBook
def list_contacts(_parent, args, _resolution) do
contacts = AddressBook.list_contacts(args)
{:ok, contacts}
end
def get_contact(_parent, %{id: id}, _resolution) do
contact = AddressBook.get_contact!(id)
{:ok, contact}
end
end
This resolver looks suspiciously like the original REST ContactController
, and this is mainly thanks to having the AddressBook
context hide away all of the complexity around preparing contact data sets for delivery to the front end. Handy!
One extra tiny change that needs to happen before we move on: did you notice in the get_contact()
function that the map that comes through as the args
parameter has atoms for keys, as opposed to the ContactController
, where the keys are strings? We’re handling that fine in the get_contact
function, but in list_contacts()
, we’re passing args
straight through to AddressBook.list_contacts()
, which is expecting a map with string keys, so we will have to update it to expect one with atom keys:
lib/phoenix_and_elm/address_book/address_book.ex
defmodule PhoenixAndElm.AddressBook do
# ...
def list_contacts(%{search: query} = params) do
# ...
end
end
This small change is the only time we should have to climb over the AddressBook
context wall.
Router
Finally, let’s expose our new GraphQL API to the world by changing the router to send /api
requests to the new schema, and /api/graphiql
requests to GraphiQL.
lib/phoenix_and_elm_web/router.ex
defmodule PhoenixAndElmWeb.Router do
# ...
scope "/api" do
pipe_through :api
forward "/graphiql", Absinthe.Plug.GraphiQL,
schema: PhoenixAndElmWeb.Schema,
interface: :simple
forward "/", Absinthe.Plug, schema: PhoenixAndElmWeb.Schema
end
scope "/", PhoenixAndElmWeb do
pipe_through :browser
get "/*path", AddressBookController, :index
end
end
Testing with GraphiQL
Speaking of GraphiQL, let’s use it to help us build the queries that we’re going to want to have Elm send to it. Navigating to http://localhost:4000/api/graphiql will bring up the GraphiQL interface, so let’s start with a GraphQL query for a single contact.
We need a query that will take in a contact ID parameter, and will return all the fields that we currently have defined in the Contact.Decoder
Elm file:
query($contactID: ID!) {
contact(id: $contactID) {
id
firstName
lastName
gender
birthDate
location
phoneNumber
email
headline
picture
}
}
The exclamation mark on
ID!
means that the field is non-nullable, so you have to provide an ID or the query will error out.
Let’s now input that in GraphiQL and fire it off to the Phoenix app, along with a contactID
parameter:
Looks pretty good to me! Now, how about for a list of contacts?
We need a query that will take in search and page number parameters, and return all the fields that we currently have defined in the ContactList.Decoder
Elm file:
query($searchQuery: String!, $pageNumber: Int!) {
contacts(search: $searchQuery, page: $pageNumber) {
entries {
id
firstName
lastName
gender
birthDate
location
phoneNumber
email
headline
picture
},
pageNumber,
totalEntries
totalPages,
}
}
And for a search query of "Barn"
, the results are…
…all of the users with a first name of Barney! Great! We now know the GraphQL queries that we want the front end to send to the back end, and now, it’s time to get them translated into Elm code! (At this point, now that the migration from controllers to resolvers is complete, it is safe to delete ContactController
from the app.)
Migrate Front End to GraphQL
Before we start, we will need a GraphQL package for Elm, and for this project, we will use elm-graphql
. Let’s install it directly in the Elm app:
cd assets/elm
elm-package install jamesmacaulay/elm-graphql
Now, since the Phoenix-side API URL has changed, the first thing we need to do is make our easiest edit, and tell Elm the new location to send requests to:
assets/elm/src/Commands.elm
module Commands exposing (apiUrl)
apiUrl : String
apiUrl =
"/api"
Contact via GraphQL
Now, let’s begin the process of getting the display of a single contact working again, starting with changing Contact.Commands
to use GraphQL when sending requests:
assets/elm/src/Contact/Commands.elm
module Contact.Commands exposing (fetchContact)
import Commands exposing (apiUrl)
import Contact.Messages exposing (ContactMsg(FetchContact))
import Contact.Request as Request
import GraphQL.Client.Http as Http
import Messages exposing (Msg(ContactMsg))
import Task exposing (Task)
fetchContact : Int -> Cmd Msg
fetchContact id =
id
|> Request.fetchContact
|> Http.sendQuery apiUrl
|> Task.attempt FetchContact
|> Cmd.map ContactMsg
Once we’ve built the GraphQL request to fetch a contact (whose Contact.Request
module we will create next), we:
- use
GraphQL.Client.Http.sendQuery
to create aTask
to send the query off to theapiUrl
- ask the Elm runtime to
attempt
to run thatTask
- send a
Msg
of typeContactMsg FetchContact
, which gets handled just like before inContact.Update
(no changes needed to that file)
Now, let’s create that Contact.Request
module to replace the Contact.Decoder
. Unlike in GraphiQL, we cannot use raw GraphQL queries in Elm-land, so we will have to port the content of the query to Elm (but let’s keep the GraphQL query that we want generated as a comment, just so we can keep our bearings):
assets/elm/src/Contact/Request.elm
module Contact.Request exposing (fetchContact, contactSpec)
import Contact.Model exposing (Contact)
import GraphQL.Request.Builder as Builder
exposing
( Document
, NonNull
, ObjectType
, Query
, Request
, ValueSpec
, field
, int
, object
, string
, with
)
import GraphQL.Request.Builder.Arg as Arg
import GraphQL.Request.Builder.Variable as Var
{-|
query($contactID: ID!) {
contact(id: $contactID) {
id
firstName
lastName
gender
birthDate
location
phoneNumber
email
headline
picture
}
}
-}
fetchContact : Int -> Request Query Contact
fetchContact id =
let
contactID =
Arg.variable (Var.required "contactID" .contactID Var.int)
contactField =
Builder.extract
(field
"contact"
[ ( "id", contactID ) ]
contactSpec
)
params =
{ contactID = id }
in
contactField
|> Builder.queryDocument
|> Builder.request params
contactSpec : ValueSpec NonNull ObjectType Contact vars
contactSpec =
Contact
|> object
|> with (field "id" [] int)
|> with (field "firstName" [] string)
|> with (field "lastName" [] string)
|> with (field "gender" [] int)
|> with (field "birthDate" [] string)
|> with (field "location" [] string)
|> with (field "phoneNumber" [] string)
|> with (field "email" [] string)
|> with (field "headline" [] string)
|> with (field "picture" [] string)
The content of the contactSpec
function pretty much lines up logically with the code that we have in Contact.Decoder
, while fetchContact
:
- builds the query step by step with the
let
expressions - creates a
GraphQL.Request.Builder.Document
for the query - creates a
GraphQL.Request.Builder.Request
from theDocument
that gets sent to theapiUrl
inContact.Commands
One final small change is to make sure that the Contact.Messages
file, which has been referencing the Http
library, now needs to reference GraphQL.Client.Http
instead:
assets/elm/src/Contact/Messages.elm
module Contact.Messages exposing (ContactMsg(..))
import Contact.Model exposing (Contact)
import GraphQL.Client.Http as Http
type ContactMsg
= FetchContact (Result Http.Error Contact)
At this point, individual contact detail pages should be displaying, so navigate to the URL of a known contact (eg http://localhost:4000/contacts/4), and you should see a page that looks something like:
The sample data in the app is generated randomly, so the contact you see from the URL above will most likely be different, but, it works! Performing a search, or navigating to the root page of the app, or doing anything that results in displaying a list of contacts will not work just yet, though, so let’s polish that task off and finish up this migration.
Contact List via GraphQL
This process will look (and be) very similar to how we migrated the contacts, so let’s briskly get through how the files will change:
assets/elm/src/ContactList/Commands.elm
module ContactList.Commands exposing (fetchContactList)
import Commands exposing (apiUrl)
import ContactList.Messages exposing (ContactListMsg(FetchContactList))
import ContactList.Request as Request
import GraphQL.Client.Http as Http
import Messages exposing (Msg(ContactListMsg))
import Task exposing (Task)
fetchContactList : Int -> String -> Cmd Msg
fetchContactList pageNumber search =
search
|> Request.fetchContactList pageNumber
|> Http.sendQuery apiUrl
|> Task.attempt FetchContactList
|> Cmd.map ContactListMsg
assets/elm/src/ContactList/Messages.elm
module ContactList.Messages exposing (ContactListMsg(..))
import ContactList.Model exposing (ContactList)
import GraphQL.Client.Http as Http
type ContactListMsg
= FetchContactList (Result Http.Error ContactList)
| Paginate Int
| ResetSearch
| SearchContacts
assets/elm/src/ContactList/Request.elm
module ContactList.Request exposing (fetchContactList)
import Contact.Request
import ContactList.Model exposing (ContactList)
import GraphQL.Request.Builder as Builder
exposing
( NonNull
, ObjectType
, Query
, Request
, ValueSpec
, field
, int
, list
, object
, with
)
import GraphQL.Request.Builder.Arg as Arg
import GraphQL.Request.Builder.Variable as Var
{-|
query($searchQuery: String!, $pageNumber: Int!) {
contacts(search: $searchQuery, page: $pageNumber) {
entries {
id
firstName
lastName
gender
birthDate
location
phoneNumber
email
headline
picture
},
pageNumber,
totalEntries,
totalPages
}
}
-}
fetchContactList : Int -> String -> Request Query ContactList
fetchContactList page search =
let
searchQuery =
Arg.variable (Var.required "searchQuery" .searchQuery Var.string)
pageNumber =
Arg.variable (Var.required "pageNumber" .pageNumber Var.int)
contactsField =
Builder.extract
(field
"contacts"
[ ( "search", searchQuery ), ( "page", pageNumber ) ]
contactListSpec
)
params =
{ searchQuery = search
, pageNumber = page
}
in
contactsField
|> Builder.queryDocument
|> Builder.request params
contactListSpec : ValueSpec NonNull ObjectType ContactList vars
contactListSpec =
let
contact =
Contact.Request.contactSpec
in
ContactList
|> object
|> with (field "entries" [] (list contact))
|> with (field "pageNumber" [] int)
|> with (field "totalEntries" [] int)
|> with (field "totalPages" [] int)
Pretty similar set of changes, right? The only real differences are the number of parameters for the query, and the contactSpec
nesting inside contactListSpec
, which is in a similar vein to the nesting of the Contact.Decoder
inside a ContactList.Decoder
.
Now, you should be able to view any page in the app that displays a list of contacts. The GraphQL migration is complete, and you can safely remove the Decoder
files from the application.
Any issues getting things to work? Have a look at the
graphql
branch and see if there are any differences from your code.
Wrapping Up
There is so much more to GraphQL than what I’ve managed to fit into this admittedly long blog post. We only dealt with queries, and did not even touch other GraphQL fundamentals like mutations, which cover modifying server-side data (though take a look at the Elm hipster stack repo for some good examples of that).
However, I hope that you enjoyed this small taste of Phoenix, Elm, and GraphQL working together, and if you join me in making further inroads with this fully functional tech stack moving forward, I would love to hear about it!
Leave a comment