You are here

How to Implement a GraphQL API in Rails

GraphQL was created by Facebook to solve nagging issues with RESTful APIs like having to make multiple roundtrips to the server to fetch required data. Tech titans like GitHub and Shopify are already using it in production but many businesses have yet to take the leap. Leigh Halliday over at Codeship will show you how to get going with the GraphQL API in Rails so you can start avoiding RESTful API headaches too. 

Getting Started

To start with, you’ll create a new Rails installation with the command: rails new landbnb --database=postgresql --skip-test. This example app will use three models: rental, a property to rent; user, the rental owner or the person who rents; and booking, a user staying at a rental for a certain period of time. You don’t need to worry about the data migrations or the definitions of these models too much. Simply take a look at the relevant files in Leigh’s GitHub repo if you’re curious. 

Installing GraphQL

This part is easy. Simply add the graphQL gm to your Gemfile and run ‘rails generate graphql:install’. This creates a new app/graphql folder where you’ll be working. There’s also a new controller with a new route in our Rails app; although we won’t need to worry about routing or controllers so much in this tutorial. Leigh strongly recommends you comment out the mutation line in app/graphql/landbnb_schema.rb until you have actually built some mutations since this can lead to errors. 

Queries

Let’s start by creating an initial query to fetch all 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

Now that you’ve define a rentals field, you can perform a query with it like this one:

query {
  rentals {
    id
  }
}

This will give you the ids of all the rentals. The rentals field in the code has two parts: a type and a resolver. The type is the type of data returned while a resolver just tells the server how to add data to the rentals field. 

Now you 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

This essentially serializes an instance of the Rental model, defining which fields can be queried and what their types are.

You’ll notice there’s no owner field associated with the model. The resolver will take care of this. 

To test this out, run this query with http://localhost:3000/graphiql in the browser and using the GraphQL tool.

Queries With Arguments

Of course, normally we’ll want to provide some parameters with our queries. So let’s see how to do that. For example, maybe we want to limit the number of rentals returned. You can do this in the rentals field like so:

# 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

Here there’s a limit argument for the rental field which must be an integer. There’s a default value there too. 

Modifying Data

Of course, we don’t just want to fetch data. We want to modify it. Here you’ll create a mutation to allow a user to sign in. The query looks like this:

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

The root type is mutation and we’ve specified that we want a JWT token in return. For this to work, you need to uncomment the mutation line the db schema file above.
Then add a signInUser field to the mutation file:

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

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

Now you need to create a separate file to handle this mutation.

# 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 in the models folder and 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

Finally, you want to authenticate the user, checking the JWT token in the header every time the user makes a subsequent request. In GraphQL, you define headers sent with each request in the config/initializers/graphiql.rb file.

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

Then you need to modify the controller to pass the current_user as context to the 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

You can now get the current user with the authorization token. We’ll show this by adding a bookRental mutation file to the mutations folder that looks like this:

# 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

Among other things, this will handle errors where the rental id is invalid or the booking dates are wrong, for example.

Conclusion

So, now you’ve defined queries, mutations and different types you can write queries with parameters and authenticate users with JWTs. The next step is to create your own GraphQL-based API. 

 

Be sure to read the next API Design article: Why Your API Documentation Shouldn't Rely on a Tool

Original Article

How to Implement a GraphQL API in Rails

 

Comments

ChadJefferson

Thank you for giving a brief explanation GRAPHQl on Rails. I was able to understand the concepts. You had explained each and every point so clearly. Demo code for authorization token halped me a lot.User request of queries are well established.