elixir

An Introduction to Absinthe

Sapan Diwakar

Sapan Diwakar on

An Introduction to Absinthe

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

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

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

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 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 that 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:

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 scalarsboolean, 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!

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!

Sapan Diwakar

Sapan Diwakar

Our guest author Sapan Diwakar is a full-stack developer. He writes about his interests on his blog and is a big fan of keeping things simple, in life and in code. When he’s not working with technology, he loves to spend time in the garden, hiking around forests, and playing outdoor sports.

All articles by Sapan Diwakar

Become our next author!

Find out more

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps