Absinthe - Resolvers
Guide to implementing efficient and maintainable resolvers in Absinthe.
Key Concepts
Basic Resolvers
defmodule MyApp.Resolvers.User do alias MyApp.Accounts
def list_users(_parent, args, _resolution) do {:ok, Accounts.list_users(args)} end
def get_user(_parent, %{id: id}, _resolution) do case Accounts.get_user(id) do nil -> {:error, "User not found"} user -> {:ok, user} end end
def create_user(_parent, %{input: input}, _resolution) do case Accounts.create_user(input) do {:ok, user} -> {:ok, user} {:error, changeset} -> {:error, changeset} end end end
Resolution Context
def current_user(_parent, _args, %{context: %{current_user: user}}) do {:ok, user} end
def current_user(_parent, _args, _resolution) do {:error, "Not authenticated"} end
Dataloader Integration
defmodule MyApp.Schema do use Absinthe.Schema
def context(ctx) do loader = Dataloader.new() |> Dataloader.add_source(MyApp.Repo, Dataloader.Ecto.new(MyApp.Repo))
Map.put(ctx, :loader, loader)
end
def plugins do [Absinthe.Middleware.Dataloader] ++ Absinthe.Plugin.defaults() end end
In type definitions
object :user do field :posts, list_of(:post) do resolve dataloader(MyApp.Repo) end end
Custom Dataloader Source
defmodule MyApp.Loaders.User do def data() do Dataloader.KV.new(&fetch/2) end
defp fetch(:posts_count, user_ids) do counts = MyApp.Posts.count_by_user_ids(user_ids)
Map.new(user_ids, fn id ->
{id, Map.get(counts, id, 0)}
end)
end end
Best Practices
-
Use Dataloader - Prevents N+1 queries
-
Keep resolvers thin - Delegate to context modules
-
Handle errors gracefully - Return meaningful error messages
-
Use middleware - For cross-cutting concerns like auth
-
Batch related queries - Use dataloader batching
Middleware
defmodule MyApp.Middleware.Auth do @behaviour Absinthe.Middleware
def call(resolution, _config) do case resolution.context do %{current_user: %{}} -> resolution _ -> resolution |> Absinthe.Resolution.put_result({:error, "Unauthorized"}) end end end
Apply to fields
field :admin_data, :string do middleware MyApp.Middleware.Auth resolve &MyApp.Resolvers.Admin.get_data/3 end
Error Handling
defmodule MyApp.Resolvers.Helpers do def handle_changeset_errors(%Ecto.Changeset{} = changeset) do errors = Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} -> Enum.reduce(opts, msg, fn {key, value}, acc -> String.replace(acc, "%{#{key}}", to_string(value)) end) end)
{:error, errors}
end end
Anti-Patterns
-
Avoid business logic in resolvers
-
Don't query database directly in resolvers
-
Avoid returning raw Ecto changesets
-
Don't skip error handling