Absinthe is a toolkit for building a GraphQL API with Elixir. It has a declarative syntax that fits really well with Elixir’s idiomatic style.

Absinthe

In today’s post - the first of a series about Absinthe - we will explore how you can use Absinthe to create a GraphQL API. But before we jump into Absinthe, let’s take a brief look at GraphQL.

GraphQL

GraphQL is a query language that allows declarative data fetching. A client can ask for exactly what they want, and only that data is returned. Instead of having many endpoints like a REST API, a GraphQL API usually provides a single endpoint that can perform different operations based on the request body.

GraphQL Schema

Schema forms the core of a GraphQL API. In GraphQL, everything is strongly typed, and the schema contains information about the API’s capabilities. Let’s take an example of a blog application. The schema can contain a Post type like this:

type Post {
  id: ID!
  title: String!
  body: String!
  author: Author!
  comments: [Comment]
}

The above type specifies that a post will have an id, title, body, author (all non-null because of ! in the type), and an optional (nullable) list of comments. Check out Schema to learn about advanced concepts like input, Enum, and Interface in the type system.

GraphQL Query and Mutation

A type system is at the heart of the GraphQL schema. GraphQL has two special types:

  1. A query type that serves as an entry point for all read operations on the API.
  2. A mutation type that exposes an API to mutate data on the server. Each schema, therefore, has something like this:

    schema {
    query: Query
    mutation: Mutation
    }

Then the Query and Mutation types provide the real API on the schema:

type Query {
  post(id: ID!): Post
}

type Mutation {
  createPost(post: PostInput!): CreatePostResult!
}

We will get back to these types when we start creating our schema with Absinthe. Read more about GraphQL’s queries and mutation.

GraphQL API

Clients can read the schema to know exactly what an API provides. To perform queries (or mutation) on the API, you send a document describing the operation to be performed. The server handles the rest and returns a result. Let’s check out an example:

query {
  post(id: 1) {
    id
    title
    author {
      id
      firstName
      lastName
    }
  }
}

The response contains exactly what we’ve asked for:

{
  "data": {
    "post": {
      "id": 1,
      "title": "An Introduction to Absinthe",
      "author": {
        "id": 1,
        "firstName": "Sapan",
        "lastName": "Diwakar"
      }
    }
  }
}

This allows for a more efficient data exchange compared to a REST API. It’s especially useful for complex fields that are rarely used in a result that takes time to compute.

In a REST API, such cases are usually handled by providing different endpoints for fetching that field or having special attributes like include=complex_field in the query param. On the other hand, a GraphQL API can offer native support by delaying the computation of that field unless it is explicitly asked for in the query.

Setting Up Your Elixir App with GraphQL and Absinthe

Let’s now turn to Absinthe and start building our API. The installation is simple:

  1. Add Absinthe, Absinthe.Plug, and a JSON codec (like Jason) into your mix.exs:

    def deps do
      [
        # ...
        {:absinthe, "~> 1.7"},
        {:absinthe_plug, "~> 1.5"},
        {:jason, "~> 1.0"}
      ]
    end
  2. Add an entry in your router to forward requests to a specific path (e.g., /api) to Absinthe.Plug:

    defmodule MyAppWeb.Router do
      use Phoenix.Router
    
      # ...
    
      forward "/api", Absinthe.Plug, schema: MyAppWeb.Schema
    end

The Absinthe.Plug will now handle all incoming requests to the /api endpoint and forward them to MyAppWeb.Schema (we will see how to write the schema below). The installation steps might vary for different apps, so follow the official Absinthe installation guide if you need more help.

Define the Absinthe Schema and Query

Notice that we’ve passed MyAppWeb.Schema as the schema to Absinthe.Plug. This is the entry point of our GraphQL API. To build it, we will use Absinthe.Schema behaviour which provides macros for writing schema. Let’s build the schema to support fetching a post by its id.

defmodule MyAppWeb.Schema do
  use Absinthe.Schema

  query do
    field :post, :post do
      arg :id, non_null(:id)
      resolve fn %{id: post_id}, _ ->
        {:ok, MyApp.Blog.get_post!(post_id)}
      end
    end
  end
end

There are a lot of things happening in the small snippet above. Let’s break it down:

  • We first define a query block inside our schema. This defines the special query type that we discussed in the GraphQL section.
  • That query type has only one field, named post. This is the first argument to the field macro.
  • The return type of the post field is post - this is the second argument to the macro. We will get back to that later on.
  • This field also has an argument named id, defined using the arg macro. The type of that argument is non_null(:id), which is the Absinthe way of saying ID! - a required value of type ID.
  • Finally, the resolve macro defines how that field is resolved. It accepts a 2-arity or 3-arity function that receives the parent entity (not passed for the 2-arity function), arguments map, and an Absinthe.Resolution struct. The function’s return value should be {:ok, value} or {:error, reason}.

Define the Type In Absinthe

In Absinthe, object refers to any type that has sub-fields. In the above query, we saw the type post. To create that type, we will use the object macro.

defmodule MyAppWeb.Schema do
  use Absinthe.Schema

  @desc "A post"
  object :post do
    field :id, non_null(:id)
    field :title, non_null(:string)
    field :author, non_null(:author)
    field :comments, list_of(:comment)
  end

  # ...
end

The first argument to the object macro is the identifier of the type. This must be unique across the whole schema. Each object can have many fields. Each field can use the full power of the field macro that we save above when defining the query. So we can define nested fields that accept arguments and return other objects. As we discussed earlier, the query itself is an object, just a special one that serves as an entry point to the API.

Using Scalar Types

In addition to objects, you can also get scalar types. A scalar is a special type with no sub-fields and serializes to native values in the result (e.g., to a string). A good example of a scalar is Elixir’s DateTime. To support a DateTime we’ll use in the schema, we need to use the scalar macro. This tells Absinthe how to serialize and parse a DateTime. Here is an example from the Absinthe docs:

defmodule MyAppWeb.Schema do
  use Absinthe.Schema

  scalar :isoz_datetime, description: "UTC only ISO8601 date time" do
    parse &Timex.parse(&1, "{ISO:Extended:Z}")
    serialize &Timex.format!(&1, "{ISO:Extended:Z}")
  end

  # ...
end

We can then use this scalar anywhere in our schema by using :isoz_datetime as the type:

defmodule MyAppWeb.Schema do
  use Absinthe.Schema

  @desc "A post"
  object :post do
    # ...
    field :created_at, non_null(:isoz_datetime)
  end

  # ...
end

Absinthe already provides several built-in scalars - boolean, float, id, integer, and string - as well as some custom scalars: datetime, naive_datetime, date, time, and decimal.

Type Modifiers and More

We can also modify each type to mark some additional constraints or properties. For example, to mark a type as non-null, we use the non_null/1 macro. To define a list of a specific type, we can use list_of/1. Advanced types like union and interface are also supported.

Wrap Up

In this post, we covered the basics of GraphQL and Absinthe for an Elixir application. We discussed the use of GraphQL and Absinthe schema, and touched on types in Absinthe. In the next part of this series, we’ll see how we can apply Absinthe and GraphQL to large Elixir applications.

Happy coding!