Of course, "best" is subjective, but objectively, Absinthe is an excellent framework for GraphQL. I'll dig into other frameworks later, but Absinthe is certainly a good place to start. Absinthe is a mature GraphQL framework for the Elixir programming language which is also excellent and perhaps underappreciated.

Elixir runs on the Erlang VM, known for creating low-latency,
distributed, and fault-tolerant systems.
- https://elixir-lang.org/

One of GraphQL's benefits as an interface is that it allows you to easily describe large quantities of data that you want fetched and returned. To that end, those characteristics of Elixir make it an excellent solution for quickly fetching large quantities of data in parallel so it can return with minimal response times. And if response times do get too long, Absinthe's support for OpenTelemetry will help you find which parts of the schema need to be optimized.

(There’s a good intro at graphql.org if you’d like background on all this)

Schema Definition

One of the great features of Absinthe is the schema definition interface. A GraphQL schema definition is a combination of both documented, related, introspectable data structures and executable code. This can make it awkward or verbose to express with normal programming languages.

Absinthe leverages Elixir's powerful macros to reduce the boilerplate and provide developers with a clean, expressive interface to the schema even when reading it in code. Consider a hypothetical schema for an ecommerce site like Etsy. Here's an example of what their schema definition might look like for a field to fetch a user by name and fields to get the user's favorites. The schema has a concise definition and the resolver functions that are defined elsewhere are easily referenced here and mapped to their location in the schema.

object :user do
  field :name, :string
  field :avatar_url, :string
  field :follower_count, :integer

  @desc "Get the items that this user has favorited"
  field :favorite_items, list_of(:items) do
    resolve(&Resolve.User.all_favorite_items/3)
  end

  @desc "Get the shops that this user has favorited"
  field :favorite_shops, list_of(:shop) do
    resolve(&Resolve.User.all_favorite_shops/3)
  end
end

query do
  @desc "Fetch a user by user name"
  field :user, :user do
    arg(:name, non_null(:string))

    resolve(&Resolve.User.get_user_by_name/3)
  end
end

Async Resolvers

Elixir has good support for concurrent operations and Absinthe leverages that to easily allow longer running resolvers to run asynchronously so that the rest of the query can proceed in parallel. Imagine that the order_history query field is slow. All it takes to run it in parallel is to wrap it with the async call.

 object :user do
   field :order_history, list_of(:order) do
-    resolve(&Resolve.User.get_user_history/3)
+    resolve(fn user, args, r ->
+      async(fn ->
+        Resolve.User.get_user_history(user, args, r)
+      end)
+    end)
   end
 end

Batching

Frameworks might have different names for this idea, but any framework that you use should support some version of the batching concept to allow you to easily address the N + 1 probem. This topic deserves its own post, but the basic idea is that when fetching related data for a set of objects, you don't want to have to make a separate query for each object in the set.

Consider an ecommerce marketplace where you've already run a search query for a set of 40 items. For each item, we want to query data for the store that the item belongs to. If we iterate over each item and run 40 queries for a shop by ID it will be too slow. Instead, we want to run one query for all the shops associated with all the items. This is batching in a nutshell. A GraphQL server will have to do this all the time so the framework needs to make it easy and Absinthe does that. Here's an example of two ways to do batching in Absinthe, first with the regular batching interface and second with the Dataloader approach.

object :item do
# Wh  field :shop_with_batch, :shop do
    resolve(fn item, _, _ ->
      batch(
        {Resolve.Shop, :batch_by_id},
        item.shop_id,
        fn shops_by_id ->
          {:ok, Map.get(shops_by_id, item.shop_id)}
        end
      )
    end)
  end

  field :shop_with_dataloader, :shop do
    # there's a little more setup code for
    # the dataloader approach that I'll have to cover another time
    resolve(dataloader(Shop))
  end
end

query do
  field :search_items, list_of(:item) do
    arg(:query, non_null(:string))

    resolve(&Resolve.Search.query_items/3)
  end
end

What about you?

Hopefully you've found something to like about the combination of Absinthe and Elixir here.

  • If you were choosing a new GraphQL framework right now, would you consider Absinthe?

  • What about other GraphQL frameworks, which is your favorite?

  • What do you think are the most important features that a GraphQL framework should have?

It would be great to hear your thoughts, just reply where you found this post or find GraphQLint on Bluesky at @graphqlint.com or on the Fediverse at @[email protected].

Reply

or to participate

Keep Reading

No posts found