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.

  1. 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.

  2. 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 a Vacation 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
    
  3. 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.

  1. First, add the absinthe, absinthe_plug, and absinthe_phoenix packages to the application’s dependencies in the mix.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
    
  2. 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 a lib/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 and place. The places query returns a list of Place objects whereas the place query takes an id argument and returns a single Place object.

    The object macro then defines the Place object type containing four fields, with each field having a name and a type. In this case, the fields of the Place object type correspond to the same fields in the Place Ecto schema (in lib/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.

  3. By convention, resolver module files live in the lib/getaways_web/resolvers directory. Create a lib/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 an id argument, so we pattern match the second argument to get the id.

    Notice that these resolver functions simply delegate to appropriate functions of the generated Vacation context module (in lib/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.

  4. 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 the Absinthe.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…

  5. Fire up the Phoenix server:

    mix phx.server
    
  6. 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…

Source Code

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!