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-webpackAs 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
PlaceEcto schema, a migration, and aVacationcontext 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:integerThen run the generated migration to create the
placesdatabase table:mix ecto.migrate -
We’ll need some example places to query, so add the following to your
priv/repo/seeds.exsfile: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_phoenixpackages to the application’s dependencies in themix.exsfile:defp deps do [ # existing dependencies {:absinthe, "~> 1.4.2"}, {:absinthe_plug, "~> 1.4.0"}, {:absinthe_phoenix, "~> 1.4.0"} ] endabsinthe 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/schemadirectory. Create alib/getaways_web/schema/schema.exfile 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 endStarting from the top,
Absinthe.Schemapulls in macros for defining the schema using idiomatic Elixir code.Then the
querymacro defines two queries:placesandplace. Theplacesquery returns a list ofPlaceobjects whereas theplacequery takes anidargument and returns a singlePlaceobject.The
objectmacro then defines thePlaceobject type containing four fields, with each field having a name and a type. In this case, the fields of thePlaceobject type correspond to the same fields in thePlaceEcto 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
resolvemacro. 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/resolversdirectory. Create alib/getaways_web/resolvers/vacation.exfile 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 endResolvers 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
placequery takes anidargument, so we pattern match the second argument to get theid.Notice that these resolver functions simply delegate to appropriate functions of the generated
Vacationcontext module (inlib/getaways/vacation.ex) and return an:oktuple. 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.exfile 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 endThis sets up two routes:
/apiand/graphiql.The
/apiroute is what GraphQL API clients use. It forwards GraphQL requests to theAbsinthe.Plugplug which runs any GraphQL documents against the specified schema.The
/graphiqlroute 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!