Ruby

How to Implement a GraphQL API in Rails

GraphQL came out of Facebook a number of years ago as a way to solve a few different issues that typical RESTful APIs are prone to. One of those was the issue of under- or over-fetching data.

Under-fetching is when the client has to make multiple roundtrips to the server just to satisfy the data needs they have. For example, the first request is to get a book, and a follow-up request is to get the reviews for that book. Two roundtrips is costly, especially when dealing with mobile devices on suspect networks.

Over-fetching is when you only need specific data, such as the name + email of a user, but since the API doesn’t know when you need, it sends you additional information which will be ignored, such as address fields, photo, etc.

With GraphQL, you describe to the server exactly what you are looking for, no more, no less. A typical request might look like this, asking for some information about rental properties along with the name of the owner:

query {
  rentals {
    id
    beds
    owner {
      name
    }
  }
}

The response from the server arrives as follows:

{
  "data": {
    "rentals": [
      {
        "id": "203",
        "beds": 2,
        "owner": {
          "name": "Berniece Anderson"
        }
      },
      {
        "id": "202",
        "beds": 1,
        "owner": {
          "name": "Zola Hilll"
        }
      }
    ]
  }
}

The request ends up looking remarkably similar to the response. We described the exact data we were looking for and that is how it arrived back to us. If you’re absolutely brand-new to GraphQL, I recommend the website https://www.howtographql.com, which provides great examples in a variety of frontend/backend technologies.

In this article, we will explore how to implement a GraphQL API in Rails, something that giants such as GitHub and Shopify are already using in production. The application we’ll be working with is available on GitHub.

Getting Started

We’ll start with a fresh Rails installation: rails new landbnb --database=postgresql --skip-test.
We’ll be working with three models for this app:

  • Rental: The house/apartment being rented
  • User: The User which owns the Rental (owner) or books a Rental (guest)
  • Booking: A User staying at a Rental for a specified period of time

Because the point of this article isn’t to cover DB migrations and model setup, please refer to the migrations and models provided in the GitHub repository. I have also created a seed file to provide us with some initial data to play around with. Simply run the command bundle exec rake db:seed.

Installing GraphQL

Now it’s time to actually get to the GraphQL part of our Rails app.

Add the graphql gem to your Gemfile and then run the command rails generate graphql:install. This will create a new app/graphql folder, which is where we’ll spend the majority of our time. It has also added a route for us along with a new controller. Unlike typical Rails apps, we’ll almost never be working inside of the controller or the routes files.

NOTE: In the app/graphql/landbnb_schema.rb file, comment out the mutation line until we have built mutations. It was giving me an error!

In GraphQL, there are three “root” types: – query: Fetching data… think of GET requests – mutation: Modifying data… think of POST or PUT requests – subscription: Real-time updates… think of ActionCable or websockets.

Queries

We’ll begin by defining our first query to fetch all of the rentals:

# app/graphql/types/query_type.rb
Types::QueryType = GraphQL::ObjectType.define do
  name "Query"

  field :rentals, !types[Types::RentalType] do
    resolve -> (obj, args, ctx) {
      Rental.all
    }
  end
end

By defining a field called rentals, it has given us the ability to perform a query with the field rentals:

query {
  rentals {
    id
  }
}

If we explore the code a little more, the field is made up of two parts: a type and a resolver.

The type is the type of data this field will return. !types[Types::RentalType] means that it will return a non-null value, which is an array of something called a Types::RentalType. We’ll look at that in a second.

We’ve also passed a block that defines a resolver. A resolver is basically us telling our code how to fill out the data for the rentals field. In this case, we’ll just include a naive Rental.all command. Don’t worry about what obj, args, ctx are, as we’ll explore them more later.

Next, we need to define what the Types::RentalType is:

# app/graphql/types/rental_type.rb
Types::RentalType = GraphQL::ObjectType.define do
  name 'Rental'

  field :id, !types.ID
  field :rental_type, !types.String
  field :accommodates, !types.Int
  # ... other fields ...
  field :postal_code, types.String

  field :owner, Types::UserType do
    resolve -> (obj, args, ctx) { obj.user }
  end
  field :bookings, !types[Types::BookingType]
end

The above code reminds me a little of defining JSON serializers. The object we are serializing in this case is an instance of the Rental model, and we’re defining which fields are available to be queried (along with their types). The owner is slightly different because we don’t actually have an owner field on the model. By providing a resolver, we can resolve owner to the object’s user field.

You can run this query by visiting http://localhost:3000/graphiql in the browser and using the GraphiQL tool, which allows you to perform GraphQL queries and explore the API.

We’ll have to create types for the User and Booking as well, but they look very similar.

Queries with arguments

What if we wanted to allow the user to provide additional data to the query they are making? For example, the ability to say how many rentals should be returned via a limit argument. This is done when defining the rentals field, which we’ll update to the code below:

# app/graphql/types/query_type.rb
Types::QueryType = GraphQL::ObjectType.define do
  name "Query"

  field :rentals, !types[Types::RentalType] do
    argument :limit, types.Int, default_value: 20, prepare: -> (limit) { [limit, 30].min }
    resolve -> (obj, args, ctx) {
      Rental.limit(args[:limit]).order(id: :desc)
    }
  end
end

What we have done is to state that the rentals field can contain a limit argument, which must be an integer. We also provided a default value and a “preparing” function to massage the argument a little bit before it is used.

You’ll notice that our resolve lambda now takes advantage of the args parameter to access the limit argument. We can now perform the query like this:

query {
  rentals(limit: 5) {
    id
  }
}

Mutations

So far, we have only queried the data, but it is time to modify it! We’ll do this by creating a mutation to allow the user to sign in. Our query will look like this:

mutation {
  signInUser(email: {email: "test6@email", password: "secret"}) {
    token
    user {
      id
      name
      email
    }
  }
}

Note that the “root” type is now mutation. We’ve provided some arguments to the signInUser field and have specified that in return we want the token (a JWT that we’ll generate) and a few fields from the user.

First, ensure that your mutation line in the app/graphql/landbnb_schema.rb file is uncommented if you had commented it out previously. Then we’ll add a signInUser field to our mutation file:

# app/graphql/types/mutation_type.rb
Types::MutationType = GraphQL::ObjectType.define do
  name "Mutation"

  field :signInUser, function: Mutations::SignInUser.new
end

And finally, we’ll write the code to handle resolving that field, which will live in its own file making it easier to test in isolation.

# app/graphql/mutations/sign_in_user.rb
class Mutations::SignInUser < GraphQL::Function
  # define the arguments this field will receive
  argument :email, !Types::AuthProviderEmailInput

  # define what this field will return
  type Types::AuthenticateType

  # resolve the field's response
  def call(obj, args, ctx)
    input = args[:email]
    return unless input

    user = User.find_by(email: input[:email])
    return unless user
    return unless user.authenticate(input[:password])

    OpenStruct.new({
      token: AuthToken.token(user),
      user: user
    })
  end
end

The AuthToken class is a small PORO that I’ve put inside of the models folder. It uses the json_web_token gem.

# app/models/auth_token.rb
class AuthToken
  def self.key
    Rails.application.secrets.secret_key_base
  end

  def self.token(user)
    payload = {user_id: user.id}
    JsonWebToken.sign(payload, key: key)
  end

  def self.verify(token)
    result = JsonWebToken.verify(token, key: key)
    return nil if result[:error]
    User.find_by(id: result[:ok][:user_id])
  end
end

Authentication

Now that we’ve provided the token in response to the signInUser mutation, we’ll expect that the token is passed in a header for subsequent requests.

With GraphiQL, you can define headers sent automatically with each request in the config/initializers/graphiql.rb file (remember, this is for development only). I’ve used the dotenv gem to store the JWT_TOKEN during development.

if Rails.env.development?
  GraphiQL::Rails.config.headers['Authorization'] = -> (_ctx) {
    "bearer #{ENV['JWT_TOKEN']}"
  }
end

We’ll now need to modify the controller to correctly pass the current_user in as the context to our GraphQL code.

# app/controllers/graphql_controller.rb
def execute
  # ...
  context = {
    current_user: current_user
  }
  #...
end

private

def current_user
  return nil if request.headers['Authorization'].blank?
  token = request.headers['Authorization'].split(' ').last
  return nil if token.blank?
  AuthToken.verify(token)
end

If we look at our bookRental mutation, we can now grab the current user using this Authorization token. First, add the bookRental field to the mutations file: field :bookRental, function: Mutations::BookRental.new. We’ll now take a look at the actual mutation code:

# app/graphql/mutations/book_rental.rb
class Mutations::BookRental < GraphQL::Function
  # define the required input arguments for this mutation
  argument :rental_id, !types.Int
  argument :start_date, !types.String
  argument :stop_date, !types.String
  argument :guests, !types.Int

  # define what the return type will be
  type Types::BookingType

  # resolve the field, perfoming the mutation and its response
  def call(obj, args, ctx)
    # Raise an exception if no user is present
    if ctx[:current_user].blank?
      raise GraphQL::ExecutionError.new("Authentication required")
    end

    rental = Rental.find(args[:rental_id])

    booking = rental.bookings.create!(
      user: ctx[:current_user],
      start_date: args[:start_date],
      stop_date: args[:stop_date],
      guests: args[:guests]
    )

    booking
  rescue ActiveRecord::RecordNotFound => e
    GraphQL::ExecutionError.new("No Rental with ID #{args[:rental_id]} found.")
  rescue ActiveRecord::RecordInvalid => e
    GraphQL::ExecutionError.new("Invalid input: #{e.record.errors.full_messages.join(', ')}")
  end
end

Notice that we also handled errors for when the Rental ID was invalid or there were validation errors with the booking (for example, missing information or invalid booking dates).

Conclusion

What we’ve looked at is how to get up and running with GraphQL in Rails. We’ve defined queries, mutations, and a number of different types. We’ve also learned how to provide arguments to fields and how to authenticate a user using JSON Web Tokens.

In my next article, we’ll look at how to guard our application from a few potential performance threats.

Reference: How to Implement a GraphQL API in Rails from our WCG partner Leigh Halliday at the Codeship Blog blog.

Leigh Halliday

Leigh is a developer at theScore. He writes about Ruby, Rails, and software development on his personal site.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button