Documentation/Tools/Absinthe Graphql/ skills /absinthe-resolvers

📖 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

elixir
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

elixir
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

elixir
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

elixir
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

  1. Use Dataloader - Prevents N+1 queries
  2. Keep resolvers thin - Delegate to context modules
  3. Handle errors gracefully - Return meaningful error messages
  4. Use middleware - For cross-cutting concerns like auth
  5. Batch related queries - Use dataloader batching

Middleware

elixir
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

elixir
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