Daniel Doubrovkine bio photo

Daniel Doubrovkine

aka dB., CTO at artsy.net, fun at playplay.io, NYC

Email Twitter LinkedIn Github

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 http://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.