Writing your own Absinthe DSL with macros and middleware

Absinthe is a great library to do graphql in Elixir. However, when writing your resolvers you may find that you are writing some boilerplate multiple times. So, in the spirit of keeping your code DRY, in this post I'll show how we can leverage middleware and a macro to write your own DSL for your Graphql api.

I assume some basic knowledge on how Absinthe works, most of the information can be found on in the guides

A common problem in writing api's is making sure that the queries are only accessible for logged in users. To do so in Absinthe we need to set a current_user on the context of a graphql query, see the guides for an example.

To make sure that our resolvers are only accessible when the current_user is set we can pattern match on its existence.

query do
    field :profile_picture, :string do
      resolve fn
        _, %{context: %{current_user: current_user}} ->
        {:ok, current_user.profile_picture_url}
        _, _ ->
        {:error, "Not logged in"}
      end
    end
end

We match on the presence of current_user and if it is not there we return an error. This can get quite repetitive however if you have a lot of authenticated fields. With some middleware we can clean this up.

# authenticated.ex
defmodule Example.Authenticated do
  @behaviour Absinthe.Middleware

  def call(resolution, _config) do
    case resolution.context do
      %{current_user: %{}} ->
        resolution

      _ ->
        Absinthe.Resolution.put_result(resolution, {:error, "Not logged in"})
    end
  end
end

The middleware implements the Absinthe.Middleware behaviour. This requires a call/2 function that gets passed the current resolution struct. In this struct we can get the context of the current query and find out if the current_user is present. Now we need to call the middleware for our field

query do
    field :profile_picture, :string do
      middleware Example.Authenticated
      resolve fn
        _, %{context: %{current_user: current_user}} ->
        {:ok, current_user.profile_picture_url}
      end
    end
end

We placed the middleware before the resolver, any non-authenticated call will be met with the error and when the current_user is present it is simply passed along to the resolver.

This is already a much cleaner option and we can leave this as is. Nonetheless, to showcase how you can use a macro to make a cleaner DSL for your app I'll give a little example.

Create a new file with your macro definition:

defmodule Example.IsAuthenticated do
  defmacro is_authenticated() do
    quote do
      middleware(Example.Authenticated, %{})
    end
  end
end

This macro does very little, I declare it and the only thing it does is call the middleware/2 we have seen earlier with the Authenticated middleware. When the macro is invoked it will be rewritten into everything in the quote block.

We can rewrite our resolver like this

import Example.IsAuthenticated 
query do
    field :profile_picture, :string do
        is_authenticated()
        resolve fn _, %{context: %{current_user: current_user}} ->
            {:ok, current_user.profile_picture_url}
        end
    end
end

Note that we need to import to macro for it to work. In our field definition the macro is_authenticated is called and rewritten into the call for the middleware. As you can imagine the middleware/2 is also a macro.

This is a very simple example of how you can use macros to do code generation for you. For graphql there are many options regarding e.g. authorization or pagination where macros can keep your code more DRY.

Photo by gil on Unsplash