A Deep Dive into Mutations with Absinthe
Tags: elixir, absinthe
Published: 2023-07-11 14:43:31

In the last two posts of this series, we saw how easy it is to integrate Absinthe into your app to provide a GraphQL API for querying data. We also shared some useful tips for building and maintaining large schemas and optimizing queries.

This post will show how we can provide an API to create GraphQL mutations with Absinthe for Elixir.

Let's get started!

## A Note on Queries Vs. Mutations in GraphQL

GraphQL places a clear distinction between queries and mutations. Technically, we can implement any query to write data to the server. But [GraphQL convention](https://graphql.org/learn/queries/#mutations) clearly separates them into different top-level `mutation` types.

## Mutations in GraphQL

First, let’s see what a typical mutation looks like:

```graphql
mutation CreatePost($post: PostCreateInput!) {
  createPost(post: $post) {
    post {
      id
    }
    errors
  }
}
```

This is a mutation to create a new post. It accepts a variable named `post` of type `PostCreateInput` (which is required because `!` follows the type name). This variable is passed into the `createPost` field inside the top-level `mutation` type.

The result of that mutation is a `post` (which can have further selections) and an `errors` array. Note that we have named the mutation `CreatePost` but that is just a client-side identifier. Here is what a sample response from this mutation looks like:

```json
{
  "data": {
    "createPost": {
      "post": {
        "id": "1"
      },
      "errors": null
    }
  }
}
```

## Input Objects with Absinthe for Elixir

Let’s see how we can implement this with Absinthe. The first thing we need is an [`input_object`](https://hexdocs.pm/absinthe/Absinthe.Type.InputObject.html) that can be used as a variable for the mutation. Let’s create one now:

```elixir
defmodule MyAppWeb.Schema do
  use Absinthe.Schema

  enum :post_state_enum do
    value :active
    value :draft
  end

  input_object :post_create_input do
    field :title, non_null(:string)
    field :author_id, non_null(:id)
    field :body, non_null(:string)
    field :state, :post_state_enum, default_value: :draft
  end
end
```

Here, we use the [`enum`](https://hexdocs.pm/absinthe/Absinthe.Type.Enum.html) macro to create an enum named `post_state_enum` with two possible values — `draft` and `active`.

We then use the `input_object` macro to define an input object named `post_create_input` which has four fields. Note that the definition of fields changes slightly from what we would use in an output type. For example, for fields inside an input object, we can add a `default_value` to a non-required field. Absinthe will fill the field in if the user doesn’t provide a value.

## Defining Mutations in Absinthe

Next, let’s define our mutation type in the schema. All individual mutations (like `createPost`) will be fields inside this mutation:

```elixir
defmodule MyAppWeb.Schema do
  use Absinthe.Schema

  mutation do
    field :create_post, non_null(:post_mutation_result) do
      arg :post, non_null(:post_create_input)
    end
  end
end
```

Here, we use the `mutation..do..end` block to define the base mutation type. And inside it, we define a field named `create_post`. This takes a single argument named `post` of the `post_create_input` type defined above. The result of this field is of type `post_mutation_result`. We haven’t defined it yet, so let’s do that now.

```elixir
object :post_mutation_result do
  field :post, :post
  field :errors, list_of(non_null(:string))
end
```

Now on to the interesting part — actually performing the mutation operation. This is similar to what we already did for queries. We will define a resolver to handle it.

<Banner lang="elixir" />

If you remember from [our previous post](https://blog.appsignal.com/2023/06/06/absinthe-for-large-elixir-applications.html), a 3-arity resolver receives the parent object, the attributes, and an `Absinthe.Resolution` struct. Let’s define that first to create the post:

```elixir
defmodule MyAppWeb.Resolvers.PostResolver do
  def create_post(_parent, %{post: attrs}, _resolution) do
    case MyAppWeb.Blog.create_post(attrs) do
      {:ok, post} ->
        {:ok, %{post: post}}
      {:error, changeset} ->
        {:ok, %{errors: translate_changeset_errors(changeset)}}
    end
  end
end
```

In the resolver, we use the `post` attributes to create a post. If that is successful, we respond with the post object (the result map should correspond to the mutation's result type — `post_mutation_result`, in our case). On an error, we respond with the `errors`. Note that [`translate_changeset_errors`](https://gist.github.com/sapandiwakar/fd1642a484826f2e02208d02c014fb55) is a custom helper function that translates `Ecto.Changeset` errors into an array of strings.

With the resolver in place, we can now plug it into our schema to create a post:

```elixir
defmodule MyAppWeb.Schema do
  use Absinthe.Schema

  mutation do
    field :create_post, :post_mutation_result do
      arg :post, non_null(:post_create_input)

      resolve &MyAppWeb.Resolvers.PostResolver.create_post/3
    end
  end
end
```

Now, when we execute the above mutation against our schema, Absinthe will call the `create_post` function. The return value from the resolver's `{:ok, value}` tuple will then be used as the mutation's result and returned to the user.

## Authorization in Your Elixir App with GraphQL and Absinthe

Up until now, we have been discussing everything as if we're using an open public API. But in most apps, this is not how things work. Often, certain APIs are only available to logged-in users, and this is even more important in the case of mutations. Let’s see how we can add authorization to our API.

First, we will need to identify the users making requests. In the [first post of this series](https://blog.appsignal.com/2023/05/16/an-introduction-to-absinthe-for-elixir.html#setting-up-your-elixir-app-with-graphql-and-absinthe), we used `Absinthe.Plug` to handle all the requests coming into the `/api` endpoint using the `MyAppWeb.Schema` schema. This doesn’t handle any user authorization for us.

Absinthe provides a [context](https://hexdocs.pm/absinthe/context-and-authentication.html) concept containing shared information that might be useful for all queries and mutations. This is passed to all resolver functions that accept an [`Absinthe.Resolution`](https://hexdocs.pm/absinthe/Absinthe.Resolution.html) struct inside the `context` field.

Let’s fix that first to identify users _before_ a request is forwarded to Absinthe.

## An Example Using Absinthe Context

Update the router in your app to execute an additional plug before forwarding a request:

```elixir
defmodule MyAppWeb.Router do
  use MyAppWeb, :router

  pipeline :api do
    plug :accepts, ["json"]
  end

  pipeline :graphql do
    plug MyAppWeb.Schema.Context
  end

  scope "/" do
    pipe_through [:api, :graphql]

    forward "/api", Absinthe.Plug, schema: MyAppWeb.Schema
  end
end
```

We've added a new `MyAppWeb.Schema.Context` plug to the requests. Let’s implement it to put a user in the Absinthe context.

```elixir
defmodule MyAppWeb.Schema.Context do
  @behaviour Plug

  import Plug.Conn

  #...

  def call(conn, _default) do
    Absinthe.Plug.put_options(conn, context: absinthe_context(conn))
  end

  def absinthe_context(conn) do
    %{conn: fetch_query_params(conn)}
    |> put_user()
  end
end
```

_**Note:** I have intentionally left out some app-specific parts of the plug. Here is the [full code of that module](https://gist.github.com/sapandiwakar/cd6b988ae8f7d59db579f228fad63210) if you are interested._

An interesting part of the code in the above code block is the use of `Absinthe.Plug.put_options/2` to set values that `Absinthe.Plug` will pass to Absinthe when executing the query/mutation. This is similar to how we use [`Plug.Conn.assign`](https://hexdocs.pm/plug/Plug.Conn.html#assign/3) to assign something on the conn.

Internally, `Absinthe.Plug` puts a private assign inside the `conn` and passes it as the third parameter to [`Absinthe.run/3`](https://hexdocs.pm/absinthe/Absinthe.html#run/3) when executing the document. The `context` option we use above is a special value that Absinthe will then pass to all resolvers inside the `Absinthe.Resolution` struct.

Now let’s update our `create_post/3` resolver function to use this context and deny the request if the user isn’t present.

```elixir
defmodule MyAppWeb.Resolvers.PostResolver do
  def create_post(
    _parent,
    %{post: attrs},
    %Absinthe.Resolution{context: %{user: nil}}
  ) do
    {:error, "unauthorized"}
  end

  def create_post(_parent, %{post: attrs}, %Absinthe.Resolution{}) do
    case MyAppWeb.Blog.create_post(attrs) do
      {:ok, post} ->
        {:ok, %{post: post}}
      {:error, changeset} ->
        {:ok, %{errors: translate_changeset_errors(changeset)}}
    end
  end
end
```

If a resolver function returns an error tuple, Absinthe will not resolve that field and adds an error to the response.

This is just one example of how to use Absinthe context. There are many more advanced potential use cases. For example, you might want to automatically set a user’s id as the post author, instead of accepting an `author_id` in the `post_create_input`.

## Middleware in Absinthe for Elixir

We have used context to authorize a user during resolution. But if we have many fields to handle, this soon becomes too cumbersome. Absinthe provides middleware to handle such cases.

For example, let's assume that several fields require a user to be present when performing an operation. Instead of updating the resolver function for all those fields, wouldn’t it be better if we could just write `middleware MyAppWeb.Schema.RequireUser` in the field definition?

We can do that using the [`middleware/2` macro](https://hexdocs.pm/absinthe/Absinthe.Middleware.html#module-the-middleware-2-macro):

```elixir
defmodule MyAppWeb.Schema do
  use Absinthe.Schema

  mutation do
    field :create_post, :post_mutation_result do
      arg :post, non_null(:post_create_input)

      middleware MyAppWeb.Schema.RequireUser

      resolve &MyAppWeb.Resolvers.PostResolver.create_post/3
    end
  end
end
```

The argument to `middleware` is a module that implements the `Absinthe.Middleware` behaviour. The module should start a `call/2` function that receives an `Absinthe.Resolution` struct as the first argument.

The second argument is whatever is passed to the `middleware/2` macro. We don’t pass anything above, so it's nil by default. The `call/2` function should return an updated `Absinthe.Resolution` struct.

### Implement `RequireUser` Middleware

Let’s implement the `RequireUser` middleware now to stop requests if a user is not present:

```elixir
defmodule SpendraWeb.Schema.RequireUser do
  @behaviour Absinthe.Middleware

  def call(%Absinthe.Resolution{state: :resolved} = resolution, _config), do: resolution

  def call(%Absinthe.Resolution{context: {user: nil}} = resolution, _config) do
    Absinthe.Resolution.put_result(
      resolution,
      {:error, "You must be logged in to access this API"}
    )
  end

  def call(%Absinthe.Resolution{} = resolution, _config), do: resolution
end
```

The middle clause is the most important. Here, we receive a context with a `nil` value for the user. We use `Absinthe.Resolution.put_result` to mark that the resolution is now complete. It does two things:

1. Marks the `state` of the resolution as `resolved`.
2. Puts the specified value as the resolution result. Here we use an error tuple to signal that this is an erroneous response.

We also have a special clause at the beginning that checks for the resolution's existing state. If you are using multiple middlewares in your app, this is important. It avoids a middleware running on a resolution that has already been resolved.

Finally, if a resolution's state is not already `resolved` and we have a user in the context, we just return the original resolution struct without any modifications. This allows execution to proceed.

### Chaining Middleware

The great thing about middleware is that it can be chained. For example, we might have several user roles and only want authors to be able to create posts. We can use multiple middleware clauses that each perform a single task before the final resolution.

```elixir
field :create_post, :post_mutation_result do
  middleware MyAppWeb.Schema.RequireUser

  middleware MyAppWeb.Schema.RequireAuthor

  resolve &MyAppWeb.Resolvers.PostResolver.create_post/3
end
```

In fact, `resolve` itself is just a middleware which roughly translates to `middleware Absinthe.Resolution, unquote(function_ast)`.

Another important point about middlewares is that they are executed in the order they are defined in a field. So in the above example, first `RequireUser` will be executed, followed by `RequireAuthor` and, finally, the resolver.

It is also possible to use middleware after a resolve call — for example, to handle resolution errors.

## Wrap Up

In this post, we created GraphQL mutations with Absinthe. We also added a custom plug to put shared information in an Absinthe context and defined middleware to handle common tasks like authorization.

In the next and final part of this series, we will discuss advanced GraphQL use cases like subscriptions (including how to implement and deliver subscriptions over a WebSocket connection).

Happy coding!

**P.S. If you'd like to read Elixir Alchemy posts as soon as they get off the press, [subscribe to our Elixir Alchemy newsletter and never miss a single post](/elixir-alchemy)!**