GraphQL

Created by Facebook. A tool to effectively fetch resources. spec

REST v.s. GraphQL?

Been to ramen shops in Japan? You buy food tickets from the machine. 2000 yen in. Click Ramen, egg, and chashu. You get 3 tickets out for each item. This is REST. Nowadays there is modern machine that you buy 3 things. You‘ll get 3 things printed on a single ticket. This is GraphQL.

GraphQL

You write a query, POST to your GraphqlController. Schema executes your query. Schema dispatches your query to a write (mutation) or read (query). Find the right class to resolve your query.

Your Query
│
└──> GraphqlController
      └──> AppSchema
            ├── MutationType
            │   └── ramenUpdate
            └── QueryType
                └── RamenType
                    ├── id
                    ├── name
                    └── price

A short intro

You run a Ramen shop. You build a website for people to know about your Ramen.

# a GraphQL query that asks what is
# name, price, defaultToppings of tsukemen
query = <<~QUERY
query {
  ramen(type: 'tsukemen') {
    name
    price
  }
}
QUERY

class RamenSchema < GraphQL::Schema
  mutation(Types::MutationType) # <--- All Writes
  query(Types::QueryType) # <--- All Reads
end

RamenSchema.execute(query)

Read from database

# QueryType is the entry to find all types in your system
module Types
  class QueryType < Types::BaseObject
    field :ramen, resolver: Queries::Ramen
  end

  class Ramen < GraphQL::Schema::Object
    field :id, type: ID, null: false # <-- ID from graphql gem
    field :name, type: String, null: false
    field :price, type: Integer, null: false
  end
end

# Resolver for Ramen
module Queries
  class Ramen < GraphQL::Schema::Resolver
    argument :type, String, required: true
    type Types::Ramen

    def resolve(type:)
      # Fetch from your PostgreSQL server, external service, anything.
      Ramen.find_by(type: type)
    end
  end
end

This is the idea, put together into a rails app. We‘re missing a controller to execute the query from a POST request:

class GraphqlController < ActionController::API
  # POST /graphql => "graphql#execute"
  def execute
    context = { current_user: Guest.new }
    variables = EnsureHash.call(params[:variables])

    result = RamenSchema.execute(
      query,
      variables: variables,
      context: context,
    )
    render json: result
  rescue StandardError => exception
    error = GraphqlError.call(exception)
    render json: error, status: 500
  end
end

module GraphqlError
  def self.call(exception)
    Logger.error(exception.message)
    {
      error: {
        message: exception.message,
        backtrace: exception.backtrace,
      }
    }
  end
end

module EnsureHash
  def self.call(maybe_hash)
    return {} unless maybe_hash

    case maybe_hash
    when String
      maybe_hash.present? ? call(JSON.parse(maybe_hash)) : {}
    when Hash, ActionController::Parameters
      maybe_hash
    else
      raise ArgumentError, "Unexpected argument: #{maybe_hash}"
    end
  end
end

What RubyGems are there?

  • graphql (the rubygem)
  • graphql-rails (for interactive webpage)

FAQ

N+1

  • graphql-batch (DataLoader pattern)
  • batch-loader

Association with scopes

More reading material

Open Source codebases to study?