Getting Started with GraphQL, Phoenix, and React

Margaret Williford, Former Developer

Article Categories: #Code, #Front-end Engineering, #Back-end Engineering

Posted on

This tutorial will show you how to configure a Phoenix app that uses GraphQL and a React front end.

I find the best way to learn a new technology is to just build something. If you want to make the most of this post, don't just read through it, but follow along and try to build out the app! A few months ago, some fellow Vigets and I set off on building a social media app for pets. Having a concrete idea of something to build makes it easier to start getting your hands dirty with a new technology rather than just aimlessly reading blog posts (perhaps like you, right now...). I won't dive into that specific social media app here, instead we'll start with a simpler app for the purpose of demonstration. The app will have users, display a list of all these users, and will live reload new users added.

We'll use Phoenix on the server side to store the users in the database, and React Apollo on the front end to make calls to the database using GraphQL. We'll add the ability to add a user, and see that new user without a reload using GraphQL's subscription functionality.

Overview #

  1. Set it up! Use Elixir's package manager to set up a new Phoenix app.
  2. Migration Time Create a database table to store users.
  3. Don't overReact Pull in a basic React starter for the front end using Create React App.
  4. API consumption yum Set up Apollo, a popular JS library for consuming a GraphQL API.
  5. Types and Resolvers Implement a GraphQL API with the help of Absinthe, a popular Elixir library for implementing a GraphQL server.
  6. AND WE'RE LIVE Enable live updates for every time a user is added, without a refresh, by opening up a socket connection and taking advantage of GraphQL's subscription functionality.

Dependencies #

Before getting started, you'll need to have Elixir installed. On MacOS, I'd recommend using Homebrew and running brew install elixir. Once Elixir's installed, install its package manager by running mix local.hex. To install phoenix, run mix archive.install hex phx_new. For the front end, you'll need node installed, and I recommend either nvm or asdf for managing node versions.

Phoenix #

To create a new Phoenix app called my_cool_app, run mix phx.new my_cool_app in the directory where you want to save the project. For Rails folks like myself, this is like rails new. It builds out the directory structure and necessary files to get started on a Phoenix application. When that's finished, it will ask if you want to Fetch and install dependencies? [Yn]. Hit enter to get all the necessary dependencies.

Phoenix assumes you're using Postgres for your database. If you haven't already, cd my_cool_app, then run mix ecto.create to create your database for this project. Ecto is a database wrapper for Elixir. Finally, in a separate tab, run mix phx.server to run your phoenix server. Go to localhost:4000 in your favorite browser and you should see the default Phoenix page. 🎉🎉🎉

Migration #

Now let's add some users. We'll run a generator that will create both the migration file and the schema file. You'll run mix phx.gen.context Accounts User users name:string email:string. Your terminal should output (with a different timestamp on the migration):

* creating lib/my_cool_app/accounts/user.ex
* creating priv/repo/migrations/20190705201815_create_users.exs
* creating lib/my_cool_app/accounts.ex
* injecting lib/my_cool_app/accounts.ex
* creating test/my_cool_app/accounts_test.exs
* injecting test/my_cool_app/accounts_test.exs

So... what is all that? We created an Accounts context. A context is a module that groups functionality. In this example, we have a User model that is a part of the Accounts context. Down the road we might add Admin and SuperAdmin models that are a part of this same Accounts context. It lets you share some code among similar models and adds a layer of organization in the folder structure of your app.

Generating the User model creates a migration file for you to fill in:

defmodule MyCoolApp.Repo.Migrations.CreateUsers do
  use Ecto.Migration

  def change do
    create table(:users) do
      add :name, :string
      add :email, :string

      timestamps()
    end

  end
end

Let's modify that slightly to make the name a required field.

create table(:users) do
   add :name, :string, null: false
   ...
 end

Next, we'll take a look at the model it generated. Let's add a validation for the required name field. Update the changeset in the autogenerated lib/my_cool_app/accounts/user.ex.

defmodule MyCoolApp.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field :name, :string
    field :email, :string

    timestamps()
  end

  @required_fields ~w(name)a
  @optional_fields ~w(email)a
  def changeset(user, attrs) do
    user
    |> cast(attrs, @required_fields ++ @optional_fields)
    |> validate_required(@required_fields)
  end
end

Last, we'll look at the autogenerated Accounts context. This gives us methods to access the database for users CRUD. No need to change anything here yet.

It also autogenerated tests for us -- yahoo! I won't get into specifics on testing in this tutorial, but you should be running them regularly with mix test.

Alright, things are looking good. Run mix ecto.migrate to run the migration. Now that we have our users table, we'll pause here and switch over to the front end.

React #

To add React on the front end, we'll use Create React App. This takes care of the build configuration for us. Run npx create-react-app client to create a client directory that contains our front end code. cd client && yarn start. This will open up your browser to localhost:3000 with the default React app page, informing you to "Edit src/App.js and save to reload."

Apollo #

We'll need some dependencies to use GraphQL on the front end. Open up client/package.json and add the following packages:

{
  ...
  "dependencies": {
    "@absinthe/socket": "^0.2.1",
    "@absinthe/socket-apollo-link": "^0.2.1",
    "apollo-boost": "^0.4.0",
    "graphql": "^14.3.1",
    "graphql-tag": "^2.10.1",
    "react": "^16.8.6",
    "react-apollo": "^2.5.6",
    "react-dom": "^16.8.6",
    "react-scripts": "3.0.1"
  },
  ...
}

What is all this? GraphQL is a query language for APIs. GraphQL lets clients request the precise set of data that they need and nothing more. This blog gives a good synopsis of the pros and cons of REST vs GraphQL. GraphQL isn't really necessary in this simple user example, but this app will teach the mechanics of setting up GraphQL endpoints for where it may be more applicable. Absinthe is a popular Elixir library for implementing a GraphQL server. Apollo is a popular JS library for consuming a GraphQL API.

Let's run a cd client && yarn install to make sure our packages are installed. Next, we'll modify client/App.js to use the Apollo client. We'll also create a Users component to show a list of users to display on the homepage.

import React from "react";
import { ApolloProvider } from "react-apollo";
import "./App.css";
import { createClient } from "./util/apollo";
import Users from "./Users";

function App() {
  const client = createClient();

  return (
    <ApolloProvider client={client}>
      <Users />
    </ApolloProvider>
  );
}

export default App;

The Users component that we reference above will live in client/src/Users.js and look like this:

import gql from "graphql-tag";
import React from "react";
import { Query } from "react-apollo";

function Users() {
  const LIST_USERS = gql`
    {
      listUsers {
        id
        name
        email
      }
    }
  `;

  return (
    <div>
      <h1>Users!</h1>
      <Query query={LIST_USERS}>
        {({ loading, error, data }) => {
          if (loading) return "Loading...";
          if (error) return `Error! ${error.message}`;

          return (
            <ul>
              {data.listUsers.map(user => (
                <li key={user.id}>
                  {user.name}: {user.email}
                </li>
              ))}
            </ul>
          );
        }}
      </Query>
      </div>
    );
  }
  export default Users;

What is Apollo doing here? Well, nothing yet. We need to create the client in client/src/util/apollo.js. We'll keep it simple to start by telling it to use its built in in memory cache, and to make requests over HTTP. When we set up subscriptions for live reloads, this will change to include some logic for when to use HTTP vs when to use sockets. We need to tell it where to make those requests to our backend API. By default, the Phoenix development server lives at localhost:4000 so we'll direct it there for local development. If we were going to push this to a production environment, we'd include logic around when to use the production URI. We get a few handy classes from Apollo Client that take care of most of this for us.

//  client/src/util/apollo.js
import { InMemoryCache } from "apollo-cache-inmemory";
import { ApolloClient } from "apollo-client";
import { HttpLink } from "apollo-link-http";

const HTTP_URI = "http://localhost:4000";
export const createClient = () => {
  return new ApolloClient({
    // we will change this later when setting up the socket 
    link: new HttpLink({ uri: HTTP_URI }),
    cache: new InMemoryCache()
  });
};

Now, if you visit localhost:3000 you should see an error: Error! Network error: Failed to fetch. The front end is looking for the GraphQL API on the back end. We haven't built that yet, though, so let's do that next.

GraphQL #

To use GraphQL with Phoenix, we need a few additional packages to mix.exs.

...
  defp deps do
    [
      {:absinthe, "~> 1.4"},
      {:absinthe_ecto, "~> 0.1.3"},
      {:absinthe_plug, "~> 1.4"},
      {:absinthe_phoenix, "~> 1.4.0"},
      {:cors_plug, "~> 2.0"},
      ...
    ]
  end
...

The first four packages are related to Absinthe, a popular Elixir library for implementing a GraphQL server. We need the CORS plug to grant permission to the client to make requests. Run mix deps.get to install the new dependencies. Now we can start building the API.

A GraphQL API is made of types and resolvers. We'll first build the Users type using a GraphQL schema language to represent an object you can fetch, and define all the fields that can be queried.

Under my_cool_app_web We'll create a schema directory to store types, and within it a account_types.ex file.

defmodule MyCoolAppWeb.Schema.AccountTypes do
  use Absinthe.Schema.Notation
  use Absinthe.Ecto, repo: MyCoolApp.Repo

  alias MyCoolAppWeb.Resolvers

  @desc "One user"
  object :user do
    field :id, :id
    field :name, :string
    field :email, :string
    field :avatar_url, :string
  end

  object :account_queries do
    @desc "Get all users"
    field :list_users, list_of(:user) do
      resolve(&Resolvers.AccountResolver.list_users/3)
    end
  end

  object :account_mutations do
    field :create_user, :user do
      arg(:name, non_null(:string))
      arg(:email, :string)

      resolve(&Resolvers.AccountResolver.create_user/3)
    end
  end

  object :account_subscriptions do
    field :user_created, :user do
    end
  end
end

First, we're defining a user object as a thing that can be queried and defining what fields are available. The account_queries object matches methods to resolvers (more on those below). Notice the role the context is playing here. If we added an Admin model, we'd include another field within the account_queries object for list_admins or even something like list_users_and_admins. Next is account_mutations. In GraphQL, mutations are how the client can create, update, or delete an object. In this example, we've included the field to create a new user. Similarly, we could add additional fields for updating and deleting users. The create_user field lets the client know that name is a required field, then maps to the appropriate resolver. Lastly, we set up account_subscriptions to enable the live updates. Every time a user is created, that will be communicated to the client via a socket connection, and our list of users will update automatically without page reload.

Ok but what are these resolvers? #

Every field on every type is backed by a function called a resolver. A resolver is a function that returns something back to the client.

defmodule MyCoolAppWeb.Resolvers.AccountResolver do
  alias MyCoolApp.Accounts

  def list_users(_parent, _args, _resolutions) do
    {:ok, Accounts.list_users()}
  end

  def create_user(_parent, args, _resolutions) do
    args
    |> Accounts.create_user()
    |> case do
      {:ok, user} ->
        {:ok, user}

      {:error, changeset} ->
        {:error, extract_error_msg(changeset)}
    end
  end

  defp extract_error_msg(changeset) do
    changeset.errors
    |> Enum.map(fn {field, {error, _details}} ->
      [
        field: field,
        message: String.capitalize(error)
      ]
    end)
  end
end

The list_users and create_user methods are we referenced in the AccountTypes module. The parent, args, resolutions params come from GraphQL itself. The underscore (_parent and _resolutions) is an Elixir convention to indicate unused variables. Both methods use functions from the Accounts context (my_cool_app/accounts.ex) that we autogenerated when we ran the users migration, and both return a tuple with the status followed by the relevant object(s) or error message. The extract_error_msg method is taking the error received from Absinthe and translating into something more easily readable.

Lastly, we need to do a few things to configure the socket connection. We'll create a new file my_cool_app_web/channels/absinthe_socket.ex. We can delete the autogenerated user_socket.ex.

defmodule MyCoolAppWeb.AbsintheSocket do
    use Phoenix.Socket
    use Absinthe.Phoenix.Socket, schema: MyCoolAppWeb.Schema

    def connect(_, socket) do
      {:ok, socket}
    end

    def id(_), do: nil
end

We also need to modify our endpoint so the API uses the right socket and the CORS plug. So we'll strip unnecessary code out of my_cool_app_web/endpoint.ex and just have the remaining:

defmodule MyCoolAppWeb.Endpoint do
    use Phoenix.Endpoint, otp_app: :my_cool_app
    use Absinthe.Phoenix.Endpoint

    socket "/socket", MyCoolAppWeb.AbsintheSocket,
      websocket: true,
      longpoll: false

    # Code reloading can be explicitly enabled under the
    # :code_reloader configuration of your endpoint.
    if code_reloading? do
      plug Phoenix.CodeReloader
    end

    plug Plug.RequestId
    plug Plug.Logger

    plug Plug.Parsers,
      parsers: [:urlencoded, :multipart, :json],
      pass: ["*/*"],
      json_decoder: Phoenix.json_library()

    plug CORSPlug

    plug MyCoolAppWeb.Router
  end

The default Phoenix router (my_cool_app_web/router.ex) will also need to change. We need to indicate that we're using an API that uses the Absinthe plug.

defmodule MyCoolAppWeb.Router do
  use MyCoolAppWeb, :router

  pipeline :api do
    plug :accepts, ["json"]
  end

  scope "/" do
    pipe_through :api

    forward "/graphiql", Absinthe.Plug.GraphiQL, schema: MyCoolAppWeb.Schema

    forward "/", Absinthe.Plug, schema: MyCoolAppWeb.Schema
  end
end

We're making an API router here and noting that we can only handle requests in JSON. This routes API requests to use our Absinthe schema, rather than the default Ecto Schema. For more details on the specifics of routing in Phoenix, check out these docs.

Next we need to build out that Absinthe schema. Absinthe is the GraphQL toolkit for Elixir, so we're building out a schema that is specific to GraphQL functionality. The Absinthe packages we installed give us some handy macros and utility functions to be able to define where to look for our queries, mutations, and subscriptions.

// my_cool_app_web/schema.ex
defmodule MyCoolAppWeb.Schema do
  use Absinthe.Schema

  import_types(Absinthe.Type.Custom)
  import_types(MyCoolAppWeb.Schema.AccountTypes)

  query do
    import_fields(:account_queries)
  end

  mutation do
    import_fields(:account_mutations)
  end

  subscription do
    import_fields(:account_subscriptions)
  end
end

If we were to build out more functionality, the query, mutation, and subscription blocks would each have several import_fields lines related to every queryable object.

That's all we need to set up the communication between front end and back end. If you visit http://localhost:3000/ the error message should be gone, but there aren't any users to see!

We can add a user by playing with the GraphiQL interface at http://localhost:4000/graphiql. You can define a query, and pass query variables right in the GUI. We need to add the createUser mutation here, and pass it a new user to add to our database.

mutation CreateUser($name: String!, $email: String) {
  createUser(name: $name, email: $email) {
    id
    name
    email
  }
}

Next we can pass some query variables (you may need to double click this tab to open it up) and create our first user. The query variables should be written in JSON since that's what we stated in our router that we required. {"name": "Beyonce", "email": "destinyschild4ever@yahoo.com"} Hit the Play ▶️ button and you've added your first user to the database. Flip back to the front end site localhost:3000, refresh, and you should see your first user listed. You've done the thing!

Live Update #

Sure, we can see users, but we have to manually refresh the page every time one is added. Who has that sort of time?! Let's build in a live reload functionality to exemplify some GraphQL coolness. Because of the socket connection it uses, GraphQL can instantly send over new or changed data and our users list will immediately reflect the change.

We'll add a Subscription component on the front end to manage subscriptions.

// client/src/Subscriber.js
import { useEffect } from "react";

const Subscriber = ({ subscribeToNew, children }) => {
  useEffect(() => {
    subscribeToNew();
  }, []);

  return children;
};

export default Subscriber;

This will make more sense once we've written the subscribeToNew method. Basically, this component is set up to know to use that method to look for new users. So let's write that method. We'll go back to the Users component to make several changes. We'll add in a form to add a new user on the page directly, write a createUser method like we did in the GraphQL GUI, and set up the subscription.

import gql from "graphql-tag";
import React from "react";
import { Query } from "react-apollo";
import produce from "immer";
import Subscriber from "./Subscriber";
import NewUser from "./NewUser";

function Users({subscribeToNew, newItemPosition, createParams}) {
  const LIST_USERS = gql`
    {
      listUsers {
        id
        name
        email
      }
    }
  `;

  const USERS_SUBSCRIPTION = gql`
    subscription onUserCreated {
      userCreated {
        id
        name
        email
      }
    }
  `

  return (
    <div>
      <h1>Users!</h1>
      <Query query={LIST_USERS}>
        {({ loading, error, data, subscribeToMore }) => {
          if (loading) return "Loading...";
          if (error) return `Error! ${error.message}`;

          return (
            <>
            // This NewUser component is the form to create a new user, we'll build that next.
            <NewUser params={createParams} />
            <Subscriber subscribeToNew={() =>
                subscribeToMore({
                  document: USERS_SUBSCRIPTION,
                  updateQuery: (prev, { subscriptionData }) => {
                    // if nothing is coming through the socket, just use the current data
                    if (!subscriptionData.data) return prev;

                    // something new is coming in! 
                    const newUser = subscriptionData.data.userCreated;

                    // Check that we don't already have the user stored.
                    if (prev.listUsers.find((user) => user.id === newUser.id)) {
                      return prev;
                    }

                    return produce(prev, (next) => {
                      // Add that new user!
                      next.listUsers.unshift(newUser);
                    });
                  },
                })
              }>
            <ul>
              {data.listUsers.map(user => (
                <li key={user.id}>
                  {user.name}: {user.email}
                </li>
              ))}
            </ul>
          </Subscriber>
          </>
          );
        }}
      </Query>
    </div>
  );
}
export default Users;

We referenced a new user component, but still need to build that. I should note that we could just keep everything within this Users.js file, but it makes for more maintainable code to pull something like this out into a separate component.

// client/src/NewUser.js
import React, { useState} from "react";
import gql from "graphql-tag";
import { Mutation } from "react-apollo"

const CREATE_USER = gql`
mutation CreateUser($name: String!, $email: String) {
   createUser(name: $name, email: $email) {
     id
   }
 }
 `;

const NewUser = ({ params }) => {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");

  const mutation = CREATE_USER;

  return (
    <Mutation mutation={mutation}
      onCompleted={() => {
        setName("");
        setEmail("");
      }}
    >
      {(submit, { data, loading, error }) => {
        return (
          <form
            onSubmit={(e) => {
              e.preventDefault();
              submit({ variables: { name, email } });
            }}
          >
            <input
              name="name"
              type="text"
              placeholder="What's your name?"
              value={name}
              onChange={(e) => setName(e.target.value)}
            />
            <input
              name="email"
              type="text"
              placeholder="What's your email?"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
            />
            <input type="submit" value="Add" />
          </form>
        );
      }}
    </Mutation>
  );
};

export default NewUser

Visit localhost:3000 and you should see the new user form at the top of the page. Enter in a user, an email, and hit add. Refresh the page, and your new user should appear.

What? No live reload? We reference the user subscription, but haven't actually written that method. Looking back at my_cool_app_web/schema/account_types.ex, we can see that the account_subscriptions block is empty. Let's fix that.

defmodule MyCoolAppWeb.Schema.AccountTypes do
  ....

  object :account_subscriptions do
    field :user_created, :user do
      config(fn _, _ ->
        {:ok, topic: "users"}
      end)

      trigger(:create_user,
        topic: fn _ ->
          "user"
        end
      )
    end
  end
end

The syntax is a little funky, but this is just setting up a subscription on the user object that is triggered every time the create user method is called.

Sockets #

The last step is to set up the socket connection for the requests. The Absinthe docs are our friend here. Following along their setup instructions, we add one file to create the socket link client/src/util/absinthe-socket-link.js

import * as AbsintheSocket from "@absinthe/socket";
import {createAbsintheSocketLink} from "@absinthe/socket-apollo-link";
import {Socket as PhoenixSocket} from "phoenix";

export default createAbsintheSocketLink(AbsintheSocket.create(
  new PhoenixSocket("ws://localhost:4000/socket")
));

Next, we need to tell Apollo to use this socket. Until now, we had just told Apollo to use HTTP. We'll make changes to client/src/util/apollo.js to use our socket connection for subscriptions, and HTTP for everything else.

import { InMemoryCache } from "apollo-cache-inmemory";
import { ApolloClient } from "apollo-client";
import { HttpLink } from "apollo-link-http";
import absintheSocketLink from "./absinthe-socket-link"
import { split } from "apollo-link";
import { hasSubscription } from "@jumpn/utils-graphql";

const HTTP_URI = "http://localhost:4000";

const link = split(
  operation => hasSubscription(operation.query),
  absintheSocketLink,
  new HttpLink({ uri: HTTP_URI })
);

export const createClient = () => {
  return new ApolloClient({
    link: link,
    cache: new InMemoryCache()
  });
};

Home stretch!! Just two more configuration steps. We need to add Phoenix as a dependency to the front end, as its a dependency of Absinthe and we're now referencing it on the front end to set up the socket connection. Add "phoenix": "^1.4.3", to the dependencies in client/package.json, run yarn install from the client directory, and recompile with yarn start.

Lastly, we need to configure a Supervisor on the backend to follow the subscriptions. We need to add a few lines to my_cool_app/lib/application.ex.

...
  def start(_type, _args) do
    import Supervisor.Spec
    # List all child processes to be supervised
    children = [
      # Start the Ecto repository
      MyCoolApp.Repo,
      # Start the endpoint when the application starts
      MyCoolAppWeb.Endpoint,
      # Starts a worker by calling: MyCoolApp.Worker.start_link(arg)
      # {MyCoolApp.Worker, arg},
      supervisor(Absinthe.Subscription, [MyCoolAppWeb.Endpoint])
    ]

    # See https://hexdocs.pm/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: MyCoolApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
...

Restart your Phoenix server, visit localhost:3000 and add a new user. No refresh, no problem!

Next Steps #

You just set up a Phoenix app, built a users table, added a users endpoint on a GraphQL server, created some users, and rendered those users on the front end with React Apollo. Could life get any better? Probably not. But this code sure could.

For starters, it's not a great idea to put gql query string templates in the React function body because they get evaluated every time. What if things like LIST_USERS and USERS_SUBSCRIPTION in the Users component were hoisted to the module level? And on the backend, this needs some serious test coverage. This [2 minute read] (https://medium.com/@cam_irmas/happy-hour-testing-phoenix-absinthe-graphql-apis-46f5bb2c0379) is a good intro to testing Phoenix and GraphQL APIs.

You've just set up a subscription using GraphQL, Phoenix, and React. I am proud. Now go forth and build something even cooler.

P.S. Now that you've made it to the end, checkout the example code here.

Related Articles