Daniel Doubrovkine bio photo

Daniel Doubrovkine

aka dB., @awscloud, former CTO @artsy, +@vestris, NYC

Email Twitter LinkedIn Github Strava
Creative Commons License

In the past few weeks I’ve convinced myself that GraphQL can work well for any micro-service. This post is a full walk-through of getting a working GraphQL API on Ruby on Rails. This should help you get started, especially if you’ve never written a line of GraphQL in your life. The code for this post is dblock/graphql-invoices.

Prerequisites

Make a bare Rails API app with rails new --api, get RSpec and RuboCop. See @c26b0e18.

GraphQL

Add gem 'graphql' to the project.

This is the graphql-ruby library, an implementation of GraphQL in Ruby and is what is used for building a GraphQL endpoint on the server. See @7649ba2b.

Defining Schema

This project will implement a dummy invoice API where clients can get invoices and create invoices.

The invoice type has an ID and some fees in cents and goes into app/graphql/types/invoice.rb.

InvoiceType = GraphQL::ObjectType.define do
  name 'Invoice'
  description 'An Invoice'
  field :id, !types.Int
  field :fee_in_cents, types.Int
end

The GraphQL ID type is a string, so we use Int.

GraphQL defines a schema with queries (eg. get invoices) and mutations (eg. create an invoice), which typically goes into app/graphql/schema.rb.

Schema = GraphQL::Schema.define do
  query Query
  mutation Mutation
end

The query root returns an invoice by ID, implemented in app/graphql/queries.rb. Notice that this takes an Int (ID) and returns our InvoiceType.

Query = GraphQL::ObjectType.define do
  name 'Query'

  field :invoice, InvoiceType do
    argument :id, !types.Int
    description 'Get an invoice by ID.'
    resolve ->(_obj, args, _ctx) {
      OpenStruct.new(
        id: args[:id],
        fee_in_cents: 20_000
      )
    }
  end
end

A mutation creates invoices in app/graphql/mutations/create_invoice_mutation.rb. Use Relay, a data fetching framework, that makes it easy.

CreateInvoiceMutation = GraphQL::Relay::Mutation.define do
  name 'createInvoice'

  input_field :fee_in_cents, !types.Int
  return_type InvoiceType

  resolve ->(_object, inputs, _ctx) {
    OpenStruct.new(
      id: 1231,
      fee_in_cents: inputs[:fee_in_cents]
    )
  }
end

This mutation was referenced from the root schema above and is linked from app/graphql/mutations.rb.

Mutation = GraphQL::ObjectType.define do
  name 'Mutation'

  field :createInvoice, field: CreateInvoiceMutation.field
end

GraphQL Controller

GraphQL accepts a single JSON payload via POST in a typical Rails controller in app/controllers/graphql_controller.rb.

class GraphqlController < ApplicationController
  def execute
    result = Schema.execute(
      query,
      variables: variables,
      context: context,
      operation_name: operation_name
    )
    render json: result
  end

  private

  def query
    params[:query]
  end

  def operation_name
    params[:operationName]
  end

  def context
    {}
  end

  def variables
    params[:variables] || {}
  end
end

The controlled needs to be routed to in config/routes.rb.

Rails.application.routes.draw do
  post '/graphql', to: 'graphql#execute'
end

GraphQL IDEs

The best way to try our app out is to use a GraphQL IDE. Run the Rails application with rails s and point the IDE to https://localhost:3000/graphql.

You can use various clients to consume the API from our applications, including graphlient or graphql-client.

Tests

Add graphlient, which is a small library built on top of graphql-client and that’s a bit easier to use.

Define a shared client context in spec/support/graphql/client.rb.

require 'graphlient'

RSpec.shared_context "GraphQL Client", shared_context: :metadata do
  let(:client) do
    Graphlient::Client.new('https://api.example.org/graphql') do |client|
      client.http do |h|
        h.connection do |c|
          c.use Faraday::Adapter::Rack, app
        end
      end
    end
  end
end

The client can fetch the schema in spec/graphql/schema_spec.rb.

require 'rails_helper'

describe 'GraphQL Schema', type: 'request' do
  include_context 'GraphQL Client'

  it 'retrieves schema' do
    expect(client.schema).to be_a GraphQL::Schema
  end
end

Fetch an invoice in spec/graphql/queries/invoice_query_spec.rb.

require 'rails_helper'

describe 'Invoice Query', type: :request do
  include_context 'GraphQL Client'

  let(:query) do
    <<-GRAPHQL
      query($id: Int!) {
        invoice(id: $id) {
          id
          fee_in_cents
        }
      }
    GRAPHQL
  end

  it 'returns an invoice' do
    response = client.execute(query, id: 42)
    invoice = response.data.invoice
    expect(invoice.id).to eq 42
    expect(invoice.fee_in_cents).to eq 20_000
  end
end

Create an invoice in spec/graphql/mutations/create_invoice_mutation_spec.rb.

require 'rails_helper'

describe 'Create Invoice Mutation', type: :request do
  include_context 'GraphQL Client'

  let(:query) do
    <<-GRAPHQL
      mutation($input: createInvoiceInput!) {
        createInvoice(input: $input) {
          id
          fee_in_cents
        }
      }
    GRAPHQL
  end

  it 'returns an invoice' do
    response = client.execute(query, input: { fee_in_cents: 42_000 })
    invoice = response.data.create_invoice
    expect(invoice.id).to eq 1231
    expect(invoice.fee_in_cents).to eq 42_000
  end
end

Tooling

GraphQL comes with some impressive tooling and IDE integration, such as with Visual Studio Code via graphql-for-vscode.

Add a Rake task to dump the project’s schema to lib/tasks/graphql/schema.rake.

namespace :graphql do
  namespace :schema do
    directory 'data'
    desc 'Dump GraphQL API schema to data/schema.graphql.'
    task dump: [:environment, 'data'] do
      File.open('data/schema.graphql', 'w') do |f|
        f.write(Schema.to_definition)
        puts "Dumped schema to #{f.path}."
      end
    end
  end
end

Add a configuration file, .gqlconfig

{
  "schema": {
    "files": "data/schema.graphql"
  },
  "query": {
    "files": [
      {
        "match": "spec/graphql/**/*.rb",
        "parser": ["EmbeddedQueryParser", { "startTag": "<<-GRAPHQL", "endTag": "GRAPHQL" }]
      }
    ]
  }
}

Install prerequisites for graphql-for-vscode.

brew update
brew install watchman
npm install @playlyfe/gql

Install graphql-for-vscode from the Visual Studio Code Marketplace, here.

Generating and Handling Errors

Read Generating and Handling Errors in GraphQL in Ruby next.