ð absinthe-resolvers
Use when implementing GraphQL resolvers with Absinthe. Covers resolver patterns, dataloader integration, batching, and error handling.
Overview
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