loading...

Ruby on Rails GraphQL API Tutorial: From 'rails new' to First Query

isalevine profile image Isa Levine ・7 min read

This week, I've been working on a takehome technical challenge asking me to deep-dive into GraphQL and create a simple API. I've never worked with GraphQL before, so I opted to stick with Ruby on Rails for learning it.

This tutorial is designed to walk through the steps to create a GraphQL API with Ruby on Rails and the Ruby gem 'graphql'.

It is largely adapted from this AMAZING tutorial by Matt Boldt, with a few notable differences:

  • I will use the Insomnia REST client for API calls instead of the 'graphiql' IDE -- if you don't already have it installed, go ahead and do that now!

  • I will start with simple Order and Payment models, but eventually (in a future article) branch into more complicated relationships (and implementing features like filtering objects with custom methods directly on the model and has_many declaration)

  • I will explore "idempotency" in GraphQL as part of Mutations (in a future article), using strategies covered in this EXCELLENT article by Todd Jefferson

Overview

In this first article, we'll go through the steps to:

  • create a Rails API
  • add some models
  • add GraphQL
  • write and execute our first GraphQL Query

GraphQL has two ways of interacting with databases

  1. Query -- this allows us to get data ("Read" in CRUD)
  2. Mutation -- this allows us to change information, including adding, updating, or removing data ("Create", "Update", "Destroy" in CRUD)

We'll keep our focus on getting the API running, and understanding our first simple Query.

Let's dive in!

What is GraphQL?

GraphQL is a query language we can use to get and mutate data in a database. It gives users a lot of control over what data you want to get back by targeting specific models and fields to return. It is also strongly typed, so you know exactly what kind of data you're receiving!

Read more about GraphQL on the project's website.

GraphQL is language-independent, so the Ruby implementation we will be using is the Ruby gem 'graphql'. This gives us a specific file structure and some command-line tools to easily add GraphQL functionality to our Rails API.

Creating the Rails app

'rails new'

Run the following command in your terminal to create a new Rails project called devto-graphql-ruby-api. Feel free to leave out any of these --skip flags, but none of them will be used:

$ rails new devto-graphql-ruby-api --skip-yarn --skip-action-mailer --skip-action-cable --skip-sprockets --skip-coffee --skip-javascript --skip-turbolinks --api

Generating models

Inside the directory, let's create our Order and Payment models:

$ rails g model Order description:string total:float
$ rails g model Payment order_id:integer amount:float

I prefer to set up my has_many-belongs_to relationships by hand, so let's make Payments belong to an Order:

# app/models/order.rb
class Order < ApplicationRecord
    has_many :payments
end
# app/models/payment.rb
class Payment < ApplicationRecord
    belongs_to :order
end

Create database

Run $ rails db:create to create the (default) SQLite3 development database.

Run migrations

Run $ rails db:migrate to add our models to the database.

Add seed data

Add a few example objects to seed our database:

# db/seeds.rb
order1 = Order.create(description: "King of the Hill DVD", total: 100.00)
order2 = Order.create(description: "Mega Man 3 OST", total: 29.99)
order3 = Order.create(description: "Punch Out!! NES", total: 0.75)

payment1 = Payment.create(order_id: order1.id, amount: 20.00)
payment2 = Payment.create(order_id: order2.id, amount: 1.00)
payment3 = Payment.create(order_id: order3.id, amount: 0.25)

Then run $ rails db:seed to add the data to the database.

Now we're ready to start adding in GraphQL on top of our models!

Adding GraphQL

Add 'graphql' gem to Gemfile

# Gemfile
gem 'graphql'

Then run $ bundle install to install the gem in the app.

Install GraphQL with 'rails generate'

Run $ rails generate graphql:install. This will add the /graphql/directory to the app's main directory, as well as a GraphQL-specific controller at /controllers/graphql_controller.rb.

Add GraphQL objects for models

We now need to create GraphQL objects to match our models:

$ rails generate graphql:object order
$ rails generate graphql:object payment

Filling out the new GraphQL files

Okay, we now have all the files and directories needed to build our first Query! But, some of those files still need some more code.

Define the GraphQL Types and their fields

GraphQL Types are defined with fields that tell us what data we can get from them:

# app/graphql/types/payment_type.rb
module Types
    class PaymentType < Types::BaseObject
        field :id, ID, null: false
        field :amount, Float, null: false
    end
end

This allows us to retrieve PaymentType objects that contain an id field (with a special ID primary key), and an amount field that will be a Float. Because both are set to null: false, receiving a Query response with nil in either field will throw an error.

Our GraphQL objects inherit from Types::BaseObject. Thus, when we define our class PaymentType < Types::BaseObject, we now have a Types::PaymentType available. We can use these custom Types to define what we get back from each field.

Let's take a look at how we can use Types::PaymentType in OrderType:

# app/graphql/types/order_type.rb
module Types
    class OrderType < Types::BaseObject
        field :id, ID, null: false
        field :description, String, null: false
        field :total, Float, null: false
        field :payments, [Types::PaymentType], null: false
        field :payments_count, Integer, null: false

        def payments_count
            object.payments.size
        end
    end
end

Several things to note here:

  • Because the Order model has columns for id, description, and total, we can simply create a field for them and retrieve their data.
  • Because of our has_many-belongs_to relationship, we can also make a payments field to return all Types::PaymentType objects belonging to each Order.
  • However, Order does NOT have a payments_count column--so we define a payments_count() method to return an integer with the length of the payments array.
    • NOTE: inside these custom field methods, we need to access the Order's payments through object.payments--don't forget that critical object!

Define fields on QueryType

We're almost ready to write that first Query, but first, we need to tell the main QueryType to expect it. When GraphQL receives a Query request (as opposed to a Mutation request), it will be routed to the QueryType class. Like with the Types above, we will define possible Query methods through fields.

Our first Query will simply be to retrieve all Orders in the database. Inside the class QueryType declaration, we'll add a field that returns an array of Types::OrderType:

# app/graphql/types/query_type.rb
module Types
    class QueryType < Types::BaseObject
        field :all_orders, [Types::OrderType], null: false

        def all_orders
            Order.all
        end
    end
end

As above, we define our all_orders() method underneath the field with the same name, and tell it to implicity return all Orders.

Everything's now set! We can open up Insomnia and write our first Query to get all Orders back from the database.

Writing our first Query

GraphQL Query format

Here's what our first Query will look like:

query {
    allOrders {
        id
        description
        total      
        payments {
            id
            amount
        }
        paymentsCount
    }
}

At the top, we define the request as a query {}.

Inside the query, we call the QueryType's all_orders via allOrders {}. Yep, don't forget to switch from snake-case to camel-case!

Inside allOrders {}, we select the fields from the Order model we want returned. These are the same fields we defined in app/graphql/types/order_type.rb. You can pick and choose which ones you want to receive!

Note that, with our payments {} field, we also have to define the fields from Types::PaymentType that we want to receive. The fields available are the ones we defined in app/graphql/types/payment_type.rb.

The paymentsCount field will run the payments_count method on Types::OrderType, and return the appropriate value.

Let's get this Query into Insomnia and test our API!

Execute Query in Insomnia

Run $ rails s to start the Rails API server at http://localhost:3000/graphql.

Open Insomnia, and create a new POST request. In the top-left corner of the request text editor, make sure the the POST request's format is set to "GraphQL Query".

Go ahead and add the code from the query above. Then send it off, and see what it returns:

screenshot of Insomnia running query and showing return data

Woo! Our data's all nice and organized--and it's exactly and ONLY what we requested!

Let's run a similar query, but with a fewer fields:

query {
    allOrders {
        description
        total      
        payments {
            amount
        }
    }
}

Result:

screenshot of Insomnia running query with fewer fields, and showing return data

Perfect! If we don't need the ids or the paymentsCount, no need to include them in the Query at all!

Conclusion

We now have a very simple API to Query data from a database using GraphQL! However, since GraphQL Queries can only retrieve data, we can't use our current code to make any changes to the database.

That's where Mutations come in! We'll cover that in the next installment. ;)

Here's the repo for the code in this article, too. Feel free to tinker around with it!

And once again -- thank you to Matt Boldt and his AWESOME Rails GraphQL tutorial for helping me get this far! <3

Any tips or advice for using GraphQL in Rails, or GraphQL in general? Feel free to contribute below!

Discussion

pic
Editor guide
Collapse
qz135636665908 profile image
Jose-Xu

Hi Isa, thanks for the great post. I was following it step by step, however for some reason I am running into an "ActionController::RoutingError (No route matches [GET] "/graphql")" error. Do you know which step might causing the issues? Thank you!

Collapse
isalevine profile image
Isa Levine Author

Hi Jose-Xu! At which step are you seeing this error? I'd guess it's probably from trying to access localhost:3000/graphql in your browser? If so, I believe the issue is that the /graphql route only accepts a POST request.

Let me know which part you're getting that error at, and I'll see what I can do to help! :)

Collapse
qz135636665908 profile image
Jose-Xu

Hi Isa, thanks for your reply, and sorry for the late reply, since I do not get any notifications. And yes the problem is exactly like what you said it appears as I trying to access localhost:3000/graphql. and it seems like it is getting a GET request. However in the route.rb I have wrote post "/graphql", to: "graphql#execute". Do you know if there are any other places that I might have made a mistake? Much appreciate!

Thread Thread
isalevine profile image
Isa Levine Author

Hi Jose-Xu, no problem at all on the late reply! :)

It might help to break down the expected behavior. When you say you are trying to access localhost:3000/graphql, do you mean:

  • you are trying to visit there in a browser and see something, or
  • you are trying to send a POST request with a GraphQL Query for a body?

The only way to access the endpoint at localhost:3000/graphql is by sending a POST request (in this tutorial's case, through Insomnia) and have the requests body be formatted as a GraphQL Query. So, trying to access localhost:3000/graphql from a browser will always return an error because it's never expecting a GET request.

On the other hand, if your issue is that you're getting errors with sending a POST request with a GraphQL Query body, then the issue is probably with the request body itself! Are you using Insomnia (or something similar, like Postman) to send the POST request? And if so, can you send a screenshot so we can look over it? :)

Thread Thread
Sloan, the sloth mascot
Comment deleted
Collapse
qz135636665908 profile image
Jose-Xu

Hi Isa, I actually found my issues, as I typed something wrong in my query_type.rb file. Thanks for made it clear that I should not have expect localhost:3000/graphql to work in the browser!

Thread Thread
isalevine profile image
Isa Levine Author

Glad to hear it Jose-Xu, and well done!

I was recreating the API step-by-step, and thought the issue might be that the GraphQL gem's rails generate graphql:install wasn't auto-creating all the files it needed to--and that would've been a WAY BIGGER PROBLEM! Very happy it was something less drastic than that. :)

Thread Thread
qz135636665908 profile image
Jose-Xu

thank you so much for help again!!!!

Collapse
otangvinhduong profile image
oTangVinhDuong

Hi I have the same error as Jose-Xu, Don't know when you made it, have you tried again?
dev.to/qz135636665908/comment/oo7f
quote:
"Hi Isa, I actually found my issues, as I typed something wrong in my query_type.rb file. Thanks for made it clear that I should not have expect localhost:3000/graphql to work in the browser!"

I don't know what error he made in query_type.rb file but I checked it very well and I got it right!
can you check this problem ? thanks

Collapse
isalevine profile image
Isa Levine Author

Hi oTangVinhDuong! If I'm not mistaken, I believe Jose-Xu's problem was trying to access localhost:3000/graphql in a browser.

Since GraphQL relies on sending POST requests only, you will need to use a tool like Insomnia, Postman, or graphiql to view the query results.

To double-check everything is working, I just used Insomnia to send a POST request to localhost:3000/graphql/ using a GraphQL body with the following query:

query {
    allOrders {
        id
        description
        total      
        successfulPayments {
            id
            amount
            status
        }
    }
}

I also confirmed that the query_type.rb file I used just now matches what is above--this is the code I just ran:

module Types
  class QueryType < Types::BaseObject
    field :all_orders, [Types::OrderType], null: false

    def all_orders
      Order.all
    end
  end
end

Let me know if you're still having issues, and I'll try to help as best I can! :)

Collapse
pabloc profile image
PabloC

Great post. Thank you so much.