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