Whether you already have a Phoenix app in production or you’re generating a fresh Phoenix app, adding a GraphQL API is both clean and easy thanks to the Absinthe GraphQL toolkit for Elixir.
Starting with a newly-generated Phoenix 1.4 app, we’ll walk step-by-step through how to add a GraphQL API layer atop an Ecto data model.
Create a Sample Phoenix App
Suppose the theme of our sample application is vacation getaways.
-
Start by generating a new Phoenix app named
getaways
:mix phx.new getaways --no-html --no-webpack
As this will be an API-only app, we don’t need any HTML views or webpack files.
-
We’ll use Ecto to store getaway places in a PostgreSQL database, which is configured by default in a new Phoenix application. In one fell swoop, generate a
Place
Ecto schema, a migration, and aVacation
context that serves as the API boundary for vacation-related resources:cd getaways mix phx.gen.context Vacation Place places name:string location:string max_guests:integer
Then run the generated migration to create the
places
database table:mix ecto.migrate
-
We’ll need some example places to query, so add the following to your
priv/repo/seeds.exs
file:alias Getaways.Repo alias Getaways.Vacation.Place %Place{ name: "Sand Castle", location: "Portugal", max_guests: 2 } |> Repo.insert! %Place{ name: "Blue Igloo", location: "Canada", max_guests: 4 } |> Repo.insert! %Place{ name: "Ski Cabin", location: "Switzerland", max_guests: 6 } |> Repo.insert!
And then load that seed data into the database:
mix run priv/repo/seeds.exs
Setup the App for GraphQL using Absinthe
Now that we have a standard Phoenix app with a Place
resource and corresponding CRUD functions in the Vacation
context, we can add a GraphQL layer on top of that foundation.
-
First, add the
absinthe
,absinthe_plug
, andabsinthe_phoenix
packages to the application’s dependencies in themix.exs
file:defp deps do [ # existing dependencies {:absinthe, "~> 1.4.2"}, {:absinthe_plug, "~> 1.4.0"}, {:absinthe_phoenix, "~> 1.4.0"} ] end
absinthe is the core package that implements the GraphQL spec. Simply put, it knows how to transform a GraphQL document and a schema into a JSON result. But it’s not concerned with how GraphQL documents are transported. That’s where the other two specialized packages come in. The absinthe_plug handles GraphQL documents sent over HTTP to a Phoenix endpoint. And the absinthe_phoenix package supports GraphQL subscriptions over Phoenix channels.
After adding those packages to
mix.exs
, fetch ‘em:mix deps.get
-
Next we need a schema file. A schema defines the queries, mutations, and subscriptions that a GraphQL API supports. The schema also specifies the types of data that are exposed by the API. For the purposes of this application, the API will support two queries and expose the place data stored in the database.
Here’s an example query to get a list of all places:
query { places { id name location } }
And here’s an example query to get the place with the given id:
query { place(id: 2) { name location maxGuests } }
With that as our goal, writing the schema is straightforward.
By convention, all schema-related files live in the
lib/getaways_web/schema
directory. Create alib/getaways_web/schema/schema.ex
file that looks like this:defmodule GetawaysWeb.Schema.Schema do use Absinthe.Schema query do @desc "Get a list of places" field :places, list_of(:place) do resolve &GetawaysWeb.Resolvers.Vacation.places/3 end @desc "Get a place by its id" field :place, :place do arg :id, non_null(:id) resolve &GetawaysWeb.Resolvers.Vacation.place/3 end end object :place do field :id, non_null(:id) field :name, non_null(:string) field :location, non_null(:string) field :max_guests, non_null(:integer) end end
Starting from the top,
Absinthe.Schema
pulls in macros for defining the schema using idiomatic Elixir code.Then the
query
macro defines two queries:places
andplace
. Theplaces
query returns a list ofPlace
objects whereas theplace
query takes anid
argument and returns a singlePlace
object.The
object
macro then defines thePlace
object type containing four fields, with each field having a name and a type. In this case, the fields of thePlace
object type correspond to the same fields in thePlace
Ecto schema (inlib/getaways/vacation/place.ex
). We’ve chosen to expose that data as part of the GraphQL API. But it’s important to note that a GraphQL object type doesn’t have to map one-for-one to an underlying Ecto schema. You get to choose which fields you want to expose, and fields can also be virtual. Moreover, the values for object types need not even come from a database! Fields of an object type can represent data from any source.So then, where does the data for a query come from and how is it fetched? Those details are encapsulated in the resolver function specified by the
resolve
macro. And to keep responsibilities properly aligned, it’s a good practice to put resolver functions in a separate module. -
By convention, resolver module files live in the
lib/getaways_web/resolvers
directory. Create alib/getaways_web/resolvers/vacation.ex
file that looks like this:defmodule GetawaysWeb.Resolvers.Vacation do alias Getaways.Vacation def places(_, _, _) do {:ok, Vacation.list_places()} end def place(_, %{id: id}, _) do {:ok, Vacation.get_place!(id)} end end
Resolvers are 3-arity functions. Most of the time all you care about is the second argument which is a map of query arguments. Remember, the
place
query takes anid
argument, so we pattern match the second argument to get theid
.Notice that these resolver functions simply delegate to appropriate functions of the generated
Vacation
context module (inlib/getaways/vacation.ex
) and return an:ok
tuple. This indirection may seem unnecessary, but resolver functions give you a nice separation of concerns. They can modify return values as needed and handle error cases, but the resolver functions stay away from direct use of Ecto as much as possible. That kind of separation serves you well as your schema gets more involved. -
With the schema and resolvers at the ready, we just need to configure the Phoenix router to handle incoming GraphQL documents. To do that, change your
lib/getaways_web/router.ex
file to look like this:defmodule GetawaysWeb.Router do use GetawaysWeb, :router pipeline :api do plug :accepts, ["json"] end scope "/" do pipe_through :api forward "/api", Absinthe.Plug, schema: GetawaysWeb.Schema.Schema forward "/graphiql", Absinthe.Plug.GraphiQL, schema: GetawaysWeb.Schema.Schema, interface: :simple end end
This sets up two routes:
/api
and/graphiql
.The
/api
route is what GraphQL API clients use. It forwards GraphQL requests to theAbsinthe.Plug
plug which runs any GraphQL documents against the specified schema.The
/graphiql
route lets us explore and interact with the GraphQL API using the GraphiQL in-browser IDE. So let’s do that… -
Fire up the Phoenix server:
mix phx.server
-
Then browse to http://localhost:4000/graphiql and you should see the GraphiQL web interface.
Query the Data
The GraphiQL web interface automatically inspects the schema and generated documentation for your GraphQL API. Click “Docs” in the upper-right corner and then in the Documentation Explorer click on “RootQueryType”. You’ll see the two possible queries defined by the schema. Now click on “Place” and you’ll see all the place fields you can ask for in a query.
To get a list of all places, paste the following GraphQL query into the left-hand text area:
query {
places {
id
name
location
}
}
Then press the Play button above the query in the right-hand text area you’ll see the JSON result which is always in the exact shape of the query:
To query for the place that has an ID of 2, asking for all the fields, run the following query:
query {
place(id: 2) {
id
name
location
maxGuests
}
}
So that’s how you add GraphQL to a Phoenix app! Of course there’s a lot more you can do with GraphQL including mutations and subscriptions, but those are topics for another day…
Unpack a Full-Stack GraphQL App Layer-By-Layer
Learn what it takes to put together a full-stack GraphQL app using Absinthe, Phoenix, and React in our Unpacked: Full-Stack GraphQL course. No need to piece together solutions yourself. Use this application as a springboard for creating your own GraphQL apps!