in Elixir, GraphQL, Phoenix

Phoenix GraphQL Tutorial with Absinthe: Add CRUD Using Mutations

GraphQL Create Read Update Delete

This is the second post in a series on using Phoenix and GraphQL to create clean and powerful API’s. In this post I will cover mutations and how they can be used to add CRUD functionality to an app. For those who are just joining us, I recommend going through the last post before starting this one. However, you can start where we left off last time by cloning the repo and checking out the checkpoint-1 branch. If you go that route you’ll want to mix run priv/repo/seeds.exs  in order to populate your database with some dummy data after you clone the repo and create your database (postgres).

Preparing the Posts Model

We will be adding CRUD functionality to our post model and will need to make some changes to ensure that everything works properly. Currently, the post changeset doesn’t allow any other fields besides :title  and :body . Let’s add :user_id  to the cast  and validate_required  functions so we can properly support associations between users and posts. Our new changeset should look like this:

Now we should be able to accept a :user_id .

Mutations

In GraphQL, there are only two types of operations: Queries and Mutations. A mutation is an operation that “mutates” the underlying data system. Mutations are how you create, read, update, or delete (CRUD) data. You can also use a mutation for something like a login operation in which you send an email and password to the server in order to generate a JWT to send back to the user (I’ll cover that in a future post). For now, we will go through each of the parts in CRUD to demonstrate how this is done in GraphQL.

Create

Our post changeset requires that we pass it 3 arguments: :title , :body , and :user_id . As a result, we will need to create a mutation that requires these 3 arguments. Open up your web/schema.ex  file and add the following code:

As you can see, we’ve added a new mutation block to the bottom of the module. Inside this block we define a :create_post  mutation. It accepts each of the arguments we need and makes them required by using the non_null  helper. This mutation block is where all of your mutations will go. Now, you may be thinking, “won’t this get super gnarly if we have a lot of mutations?” The answer is yes. Fortunately, Absinthe provides us with some helper functions to clean up our code. I’ll be covering them in more detail in a future post but for the curious you can check them out here. Also, for some examples of how import_types  and import_fields  work you can see them in action in this test file.

Let’s now define MyApp.PostResolver.create/2  to get this mutation working:

With that change we should now be able to create new posts. Open up GraphiQL at http://localhost:4000/graphiql  and lets try adding a post. Run the following operation:

It works! Congratulations, you’ve added your first mutation. One thing to note here is that we can ask the mutation to query data about the new post after it’s been created. In this example we are asking the mutation to give us the new :id .

We can skip the R in CRUD since we’ve already covered querying for data so let’s move on to adding update functionality.

Update

Before we can implement update functionality I need to introduce a new concept. GraphQL mutation arguments can be one of a few types of data (the full list). One of those types is the input_object . It is similar to the other GraphQL Objects that we’ve made previously but it represents an object of data that is passed to a mutation as an argument. Let’s create one to represent the  post_params for updating a post and then we will define the :update_post  mutation:

Our :update_post  mutation requires two arguments: a post :id  and data to update a post in the form of the :post  argument. Let’s now add the MyApp.PostResolver.update/2  function:

If we jump back into GraphiQL we can run the following operation to see this in action:

With that we now have CRU! Let’s finish it off by adding delete functionality.

Delete

Our last task is to add the ability to delete posts. We can do this with a delete mutation. Let’s add the following code to web/schema.ex :

Our :delete_post  mutation will accept one argument: a post :id.  Let’s now add the MyApp.PostResolver.delete/2  function:

Let’s test it out. Go back into GraphiQL and run the following operation:

It works! Now we can delete posts.

Conclusion

In this post we built off of the last post and added CRUD functionality to our API. As you can see, it’s relatively painless to start adding mutations to a GraphQL API in Phoenix with the excellent Absinthe library.

I’ve added the code from this tutorial to the repo and it will be under the branch checkpoint-2. As always, if you have any questions feel free to reach out. I’m happy to help in any way that I can.

You can find the next post in the series here.

  • jacortinas

    All of your tutorial posts have been informative and well-written! Please keep these coming because I know I’m learning a lot. Thank you!

    • Hey thanks! I really appreciate that. I’ve got some more in the works and will hopefully have them out soon!

  • collegeimprovements me

    Your Graphiql tutorials are really really really helpful to me. I hope you keep creating them. I’m the only elixir developer at our bank. So, people are naturally reluctant to adopt it but after following your tutorials they are really interested in Graphql. It’s a huge thing from my point of view.

    There are few things which I’m not able to solve. As we use guardian, I’m not sure where to put the forward graphiql call in the router. As inside the scope(that’s using auth pipeline) it shows not authenticated and outside it just shows the name of the objects(users, posts etc) but doesn’t work! Is there anything that I can try?

    • The next post I’m working on goes through authenticating GraphQL requests with Guardian. It should be up in the next few days so stay tuned!

      • collegeimprovements me

        Somehow you read my mind! I just don’t know how.

  • Pingback: Phoenix GraphQL Tutorial with Absinthe - Ryan Swapp()

  • Aurélien Scoubeau

    That’s very informative. I went through a similar setup but without Phoenix (Ecto + Plug + Absinthe). It’s actually very similar since Phoenix does leverage those powerful libs.
    One thing I didn’t get yet is that Absinthe expects a simple text query (with the proper content-type). But when using a graphql client (like apollo for vue / react), it already parses the graphql AST and send in a complex data structure: Absinthe is not happy to receive it. Do you have any experience with that?

    Thanks and keep up the good work!

    • I haven’t dug super deep into how the Apollo client works so I can’t say for sure what it does under the covers. Check out this video from the GraphQL Summit though. https://youtu.be/1Fg_QtzI7SU He talks a little bit about how Relay takes a string query defined in your code, turns it into AST, does some fancy caching stuff, and then turns the AST back into a string to send to the server. I’d imagine that Apollo does something similar.

      • Aurélien Scoubeau

        Thanks for the video!

        In the meantime I solved my issue: just forgot the plug Plug.Parsers in my router… Then it works like a charm! I can enjoy Apollo as well 🙂

  • Ryan Mes

    Great post.

    The only thing I would like to poke at is you overlook the concept of validations. In any business application you generally want to validate data being saved into the database. You can do this on the client, but it’s good practice to make sure you catch it at the server level.

    Do you have any recommendation? I have been searching for a while now and throwing exceptions in my resolvers just doesn’t feel like a good solution and you can’t throw multiple exceptions either. I have checked out another post that includes business logic errors in a response type which seemed pretty reasonable.

    Any suggestions?

    • How familiar are you with Elixir/Phoenix? It is almost never a good idea to throw an exception. The standard approach is to either return {:ok, data} for a success or {:error, reason} for an error. Absinthe is no exception. An Absinthe resolver expects you to return either of those tuples and then handles them accordingly. So, in this example we are actually validating the data on the server but it is not obvious to those unfamiliar with Ecto (GraphQL automatically validates all our data too). An Ecto changeset is how we are doing validation in this instance. For example, in our update resolver we use a changeset to validate our data and then pass it to Repo.update:

      def update(%{id: id, post: post_params}, _info) do
      Repo.get!(Post, id)
      |> Post.changeset(post_params)
      |> Repo.update
      end

      If the validation fails then this resolver will return {:error, changeset} where the changeset will contain all the errors. I didn’t explain this in the post because its in introductory type post but you would need to pull the errors out of that changeset and convert it to a string that can be sent to the client. This can easily be done with the Ecto.Changeset.traverse_errors function https://hexdocs.pm/ecto/Ecto.Changeset.html#traverse_errors/2

      Hopefully that answered your question! Happy to help if you still have questions.

      • Ryan Mes

        I am not familiar at all, but it seems that the principles still apply to me and what you are saying is easy enough to follow. I found this article that sort of covers what you are saying – https://medium.com/@tarkus/validation-and-user-errors-in-graphql-mutations-39ca79cd00bf#.nd8zyybh0
        In your case, how do you handle returning two different objects for a single graphql query? (sometimes you return a data object and sometimes an error) … I am more confused how to handle things from the graphql side as opposed to Elixir/phoenix . Also do you agree with the comments in the other article? They feel like you should use the errors array that failed query responses return. In my opinion creating a separate response object to return data and errors seems cleaner, which is similar to what you are suggesting? I have such a headache trying to figure out the best way to go about this!!

  • This is one of the best overviews I found so far, thanks so much Ryan!

    I am currently implementing a couple of Ecto schemas with lots of fields as GQL endpoints. Putting Ecto fields into GQL mutation args and schema fields feels like so much boilerplate — do you have any hints how to reduce redundancy here? I wonder if one could somehow extract Ecto fields and transform them through a safe-for-GQL-whitelist…

  • ws_ubi

    Note from my experience following this. This wouldn’t work unless you handle the Ecto.Changeset in the response. If I am not mistaking it will blow up because you can’t respond with an Ecto.Changeset so either you use middleware or format the response.

    Right?!