An Introduction to Absinthe
Sapan Diwakar
Posted on May 23, 2023
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:
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:
- A
query
type that serves as an entry point for all read operations on the API. - 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 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:
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 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:
- Add Absinthe,
Absinthe.Plug
, and a JSON codec (like Jason) into yourmix.exs
:
def deps do
[
# ...
{:absinthe, "~> 1.7"},
{:absinthe_plug, "~> 1.5"},
{:jason, "~> 1.0"}
]
end
- Add an entry in your router to forward requests to a specific path (e.g.,
/api
) toAbsinthe.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, namedpost
. This is the first argument to thefield
macro. - The return type of the
post
field ispost
— 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 thearg
macro.The type of that argument isnon_null(:id)
, which is the Absinthe way of sayingID!
— a required value of typeID
. - 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 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
.
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:
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!
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!
Posted on May 23, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.