An Introduction to Absinthe
Tags: elixir
Published: 2023-05-23 11:41:57

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.

In today’s post — the first of a series on 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 multiple 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:

```graphql
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](https://graphql.org/learn/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:

```graphql
schema {
  query: Query
  mutation: Mutation
}
```

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

```graphql
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 mutations](https://graphql.org/learn/queries/).

### GraphQL API

Clients can read the schema to know exactly what an API provides. To perform queries (or mutations) 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:

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

The response contains exactly what we've asked for:

```json
{
  "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 rarely used complex fields 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`:

   ```elixir
   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`:

   ```elixir
   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](https://hexdocs.pm/absinthe/installation.html) 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`](https://hexdocs.pm/absinthe/Absinthe.Schema.html) behaviour which provides macros for writing schema. Let’s build the schema to support fetching a post by its id.

```elixir
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](https://hexdocs.pm/absinthe/Absinthe.Schema.Notation.html#field/4).
- 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](https://hexdocs.pm/absinthe/Absinthe.Schema.Notation.html#arg/3).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](https://hexdocs.pm/absinthe/Absinthe.Schema.Notation.html#resolve/1) 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](https://hexdocs.pm/absinthe/Absinthe.Resolution.html) 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](https://hexdocs.pm/absinthe/Absinthe.Schema.Notation.html#object/3).

```elixir
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 saved above when defining the query. So we can define nested fields that accept arguments and return other `object`s.

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`](https://hexdocs.pm/elixir/1.13/DateTime.html).

To support a `DateTime` that we'll use in the schema, we need to use the [`scalar` macro](https://hexdocs.pm/absinthe/Absinthe.Schema.Notation.html#scalar/3). This tells Absinthe how to serialize and parse a `DateTime`.

Here is an example from the [Absinthe docs](https://hexdocs.pm/absinthe/Absinthe.Schema.Notation.html#scalar/3-examples):

```elixir
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:

```elixir
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](https://hexdocs.pm/absinthe/1.1.5/Absinthe.Type.BuiltIns.html) — `boolean`, `float`, `id`, `integer`, and `string` — as well as some [custom scalars](https://hexdocs.pm/absinthe/Absinthe.Type.Custom.html): `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`](https://hexdocs.pm/absinthe/Absinthe.Schema.Notation.html#non_null/1) macro. To define a list of a specific type, we can use [`list_of/1`](https://hexdocs.pm/absinthe/Absinthe.Schema.Notation.html#list_of/1).

Advanced types like [union](https://hexdocs.pm/absinthe/Absinthe.Schema.Notation.html#union/3) and [interface](https://hexdocs.pm/absinthe/Absinthe.Schema.Notation.html#interface/3) 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!

**P.S. If you'd like to read Elixir Alchemy posts as soon as they get off the press, [subscribe to our Elixir Alchemy newsletter and never miss a single post](/elixir-alchemy)!**