Build a simple GraphQL API in Rails

Wayne Chu

Wayne Chu

ID: wayne5540

wayne.5540@gmail.com

Korea & Japan

China

Philippines

Taiwan

About this talk

https://github.com/wayne5540/graph_blog

http://bit.ly/graphqlroro

Why

Github released their GraphQL API Alpha at Sep-2016

  • News:
    http://githubengineering.com/the-github-graphql-api/
  • Docs:
    https://developer.github.com/early-access/graphql/
  • Gem:
    https://github.com/rmosolgo/graphql-ruby

What

  • About GraphQL
    • What, Why, How
       
  • Build a basic API with Rails
    • Query
    • Mutation
    • Unit Test
    • E-to-E test
       
  • Some best practices

About GraphQL

What's GraphQL

Best way to learn: http://graphql.org/learn/

 

GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.

- graphql.org

A query language for your API

A query language for your API

What's that means?

// graphql-query
query {
  me: user(id: 1) {
    name
    user_name: name
    created_at
  }
}
{
  data: {
    me: {
      name: "Wayne"
      user_name: "Wayne",
      created_at: 1477102527
    }
  }
}

POST https://api.example.com/?query=

JSON Response

Request Params

graphql-query

So...why?

  1. I can get many resources in a single request

  2. Customise response to what I want

As Frontend

As Backend

  1. Versionless API

  2. No API documentation

    • Introspection system

REST vs GraphQL vs RPC

## REST

GET  https://myblog.com/articles
GET  https://myblog.com/articles/1
POST https://myblog.com/articles?title=hello

## RPC

GET  https://myblog.com/getAllArticles
GET  https://myblog.com/getArticle?id=1
POST https://myblog.com/createArticle?title=hello

## GraphQL

GET  https://myblog.com/graphql?query=graphql_query_here
POST https://myblog.com/graphql?query=graphql_query_here

Thinking in Graphs

Build a basic API with Rails

User Story

As an User...

[  ] I need an account so I can use API.

[  ] I can get my account information from API.

I need an account so I can use API.

Devise

Clearance

$ rails generate devise:install
$ rails generate devise User
$ rake db:migrate
$ rails generate clearance:install
$ rails generate clearance:routes
$ rake db:migrate

User Story

As an User...

[V] I need an account so I can use API.

[  ] I can get my account information from API.

🎉

I can get my account information from API.

query {
  me: viewer {
    id
    email
    created_at
  }
}
{
  "data": {
    "me": {
      "id": 1,
      "email": "wayne.5540@gmail.com",
      "created_at": 1477206061
    }
  }
}

query

response

Settings

# Core gem
gem 'graphql', '~> 1.0.0'

# Awesome gem to build a graphql api explorer, not necessary
gem 'graphiql-rails'

# Simple approach for User#api_token
gem 'has_secure_token'

group :development, :test do
  gem 'rspec-rails', '~> 3.5'
  gem 'shoulda-matchers'
  gem 'factory_girl_rails'
  gem 'faker'
end

User Model

class User < ActiveRecord::Base
  include Clearance::User
  has_secure_token :api_token
end

# == Schema Information
#
# Table name: users
#
#  id                 :integer          not null, primary key
#  created_at         :datetime         not null
#  updated_at         :datetime         not null
#  email              :string           not null
#  encrypted_password :string(128)      not null
#  confirmation_token :string(128)
#  remember_token     :string(128)      not null
#  api_token          :string

Open API endpoint

# config/routes.rb
Rails.application.routes.draw do
  post "graphql" => "graphqls#create"
end
class GraphqlsController < ApplicationController
  before_action :authenticate_by_api_token!

  def create
    query_string = params[:query]
    query_variables = JSON.load(params[:variables]) || {}
    context = { current_user: current_user }
    result = Schema.execute(query_string, variables: query_variables, context: context)
    render json: result
  end
end

POST /graphql?query=graphql-query

headers { "Authorization" => "Token #{user.api_token}" }

Time to know GraphQL type system

query {
  me: viewer {
    id
    email
    created_at
  }
}

Schema

QueryType

UserType

query {
  me: viewer {
    id
    email
    created_at
  }
}

Special type: Query and Mutation

Query: entry point for immutable request (Thinks as GET request)

Mutation: entry point for mutable request (Think as POST request)

Scope: Schema

Schema

query {
  me: viewer {
    id
    email
    created_at
  }
}

Custom response name

Field: viewer

Type: UserType

Scope: QueryType

QueryType

query {
  me: viewer {
    id
    email
    created_at
  }
}

Field: id

Type: IntType

Scope: UserType

Field: email

Type: StringType

Field: created_at

Type: IntType

UserType

Define UserType

# app/graph/types/user_type.rb
UserType = GraphQL::ObjectType.define do
  name "User"
  description "A user"

  field :id, types.Int
  field :email, types.String
  field :updated_at do
    type types.Int

    resolve -> (obj, args, ctx) {
      obj.updated_at.to_i
    }
  end
  field :created_at do
    type types.Int

    resolve -> (obj, args, ctx) {
      obj.created_at.to_i
    }
  end
end

Test example for UserType

RSpec.describe UserType do
  let(:user) { create(:user) }

  describe '.fields' do
    subject { described_class.fields }
    specify do
      expect(subject.keys).to match_array(%w(id email updated_at created_at))
    end
  end

  # ...

  describe '#created_at' do
    subject { described_class.fields['created_at'] }

    specify do
      expect(subject.type).to be GraphQL::INT_TYPE
    end

    specify do
      expect(subject.resolve(user, nil, nil)).to eq(user.created_at.to_i)
    end
  end
end

Define QueryType

# app/graph/types/query_type.rb
QueryType = GraphQL::ObjectType.define do
  name "Query"
  description "The query root of this schema"

  field :viewer do
    type UserType
    description "Current user"
    resolve ->(obj, args, ctx) {
      ctx[:current_user]
    }
  end
end

Entry point for all query

class GraphqlsController < ApplicationController
  before_action :authenticate_by_api_token!

  def create
    query_string = params[:query]
    query_variables = params[:variables] || {}
    context = { current_user: current_user }
    result = Schema.execute(query_string, variables: query_variables, context: context)
    render json: result
  end
end

return user and pass it to UserType as it's object

# app/graph/types/query_type.rb
QueryType = GraphQL::ObjectType.define do
  name "Query"
  description "The query root of this schema"

  field :viewer do
    type UserType
    description "Current user"
    resolve ->(obj, args, ctx) {
      ctx[:current_user]
    }
  end
end
# app/graph/types/user_type.rb
UserType = GraphQL::ObjectType.define do
  name "User"
  description "A user"

  # ...

  field :created_at do
    type types.Int

    resolve -> (obj, args, ctx) {
      obj.created_at.to_i
    }
  end
end

Return user and pass it to UserType as object

Test example for QueryType

# spec/graph/types/query_type_spec.rb
RSpec.describe QueryType do
  let(:user) { create(:user) }

  describe '.fields' do
    subject { described_class.fields }

    specify do
      expect(subject.keys).to match_array(%w(viewer))
    end
  end
end
RSpec.describe 'QueryType' do
  # ...

  describe '#viewer' do
    subject { described_class.fields['viewer'] }
    
    specify do
      expect(subject.type).to be UserType
    end

    it 'passes current_user from context to UserType' do
      expect(subject.resolve(nil, nil, { current_user: user })).to eq(user)
    end
  end
end

Final, define Schema

# app/graph/schema.rb

Schema = GraphQL::Schema.define do
  query QueryType
end
query {
  me: viewer {
    id
    email
    created_at
  }
}
Schema.execute(query_string, variables: query_variables, context: context)

End to End test

RSpec.describe Schema do
  let(:user) { create(:user) }
  let(:context) { { current_user: user } }
  let(:variables) { {} }
  let(:result) do
    Schema.execute(
      query_string,
      context: context,
      variables: variables
    )
  end

  describe "viewer" do
    let(:query_string) do
      %|
        query {
          me: viewer {
            email
          }
        }
      |
    end

    context "when there's no current user" do
      let(:context) { {} }

      it "is empty" do
        expect(result["data"]["me"]).to eq(nil)
      end
    end

    context "when there's a current user" do
      it "shows user's email" do
        expect(result["data"]["me"]["email"]).to eq(user.email)
      end
    end
  end
end

Make sure we autoload it

# config/application.rb
module GraphBlog
  class Application < Rails::Application
    # ...
    config.autoload_paths << Rails.root.join('app', 'graph', 'types')
  end
end

Setup GraphiQL

# config/initializers/graphiql.rb

GraphiQL::Rails.config.headers['Authorization'] = -> (context) {
  "Token #{context.request.env[:clearance].current_user.try(:api_token)}"
}
# config/routes.rb

Rails.application.routes.draw do
  # ...
  mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/graphql"
  # ...
end

User Story

As an User...

[V] I need an account so I can use API.

[V] I can get my account information from API.

😀

Go play around with graphiql

query {
  viewer {
    id
    email
  }
}
query {
  me: viewer {
    ...userFields
  }
  alsoIsMe: viewer {
    ...userFields
  }
}

fragment userFields on User {
  user_id: id
  email
}

Example

User Story 2!

As an User...

[  ] I can create Post from API.

[  ] I can get any of my Post from API.

[  ] I can get Post list from API.

class Post < ActiveRecord::Base
  belongs_to :user
end

# == Schema Information
#
# Table name: posts
#
#  id         :integer          not null, primary key
#  user_id    :integer
#  title      :string
#  content    :text
#  created_at :datetime         not null
#  updated_at :datetime         not null
#
# Indexes
#
#  index_posts_on_user_id  (user_id)
#

Define our Post model

I can create Post from API

mutation {
  createPost(input: { title: "Hello", content: "Hello World!" }) {
    post {
      title
      content
    }
  }
}

Create post

{
  "data": {
    "createPost": {
      "post": {
        "title": "Hello",
        "content": "Hello World!"
      }
    }
  }
}

Query

Response

mutation {
  createPost(input: { title: "Hello", content: "Hello World!" }) {
    post {
      title
      content
    }
  }
}

Schema

MutationType

PostType

mutation {
  createPost(input: { title: "Hello", content: "Hello World!" }) {
    post {
      title
      content
    }
  }
}

Special type: Query and Mutation

Query: entry point for immutable request (Thinks as GET request)

Mutation: entry point for mutable request (Think as POST request)

Scope: Schema

Schema

mutation {
  createPost(input: { title: "Hello", content: "Hello World!" }) {
    post {
      title
      content
    }
  }
}

MutationType

Scope: MutationType

Field: createPost

Type: CreatePostPayload (dynamic generated)

Arguments: Input

Field: post

Type: PostType

mutation {
  createPost(input: { title: "Hello", content: "Hello World!" }) {
    post {
      title
      content
    }
  }
}

Scope: PostType

Field: title

Type: StringType

PostType

Field: content

Type: StringType

Define PostType

# app/graph/types/post_type.rb
PostType = GraphQL::ObjectType.define do
  name "Post"
  description "A post"

  field :id, types.Int
  field :title, types.String
  field :content, types.String
  field :updated_at do
    type types.Int

    resolve -> (obj, args, ctx) {
      obj.updated_at.to_i
    }
  end
  field :created_at do
    type types.Int

    resolve -> (obj, args, ctx) {
      obj.created_at.to_i
    }
  end
end

Define MutationType

# app/graph/types/mutation_type.rb

MutationType = GraphQL::ObjectType.define do
  name "Mutation"
  description "The mutation root for this schema"

  field :createPost, field: CreatePostMutation.field
end

GraphQL::Relay::Mutation

A simple way to create a mutation field

Define CreatePostMutation

# app/graph/types/create_post_mutation.rb

CreatePostMutation = GraphQL::Relay::Mutation.define do
  name "CreatePost"

  input_field :title, !types.String
  input_field :content, types.String

  return_field :post, PostType

  resolve -> (object, inputs, ctx) {
    post = ctx[:current_user].posts.create(title: inputs[:title], content: inputs[:content])

    {
      post: post
    }
  }
end

User Story 2!

As an User...

[V] I can create Post from API.

[  ] I can get any of my Post from API.

[  ] I can get Post list from API.

Go play around with graphiql

mutation {
  createPost(input: {title: "Hello", content: "World!"}) {
    post {
      id
      title
      content
    }
  }
}
{
  "post": {
    "title": "Hello",
    "content": "World!"
  }
}

Example

Variable

mutation newPost($post: CreatePostInput!) {
  createPost(input: $post) {
    post {
      id
      title
      content
    }
  }
}

User Story 2!

As an User...

[V] I can create Post from API.

[O] I can get any of my Post from API.

  => basically same as viewer

[O] I can get Post list from API.

  => keyword: ListType

Some Best Practices

  • Interface
  • Don't test Schema
  • Use relay module

Interfaces

# app/graph/types/user_type.rb
UserType = GraphQL::ObjectType.define do
  name "User"
  description "A user"

  field :id, types.Int
  field :email, types.String
  field :updated_at do
    type types.Int

    resolve -> (obj, args, ctx) {
      obj.updated_at.to_i
    }
  end
  field :created_at do
    type types.Int

    resolve -> (obj, args, ctx) {
      obj.created_at.to_i
    }
  end
end
# app/graph/types/post_type.rb
PostType = GraphQL::ObjectType.define do
  name "Post"
  description "A post"

  field :id, types.Int
  field :title, types.String
  field :content, types.String
  field :updated_at do
    type types.Int

    resolve -> (obj, args, ctx) {
      obj.updated_at.to_i
    }
  end
  field :created_at do
    type types.Int

    resolve -> (obj, args, ctx) {
      obj.created_at.to_i
    }
  end
end
# app/graph/types/active_record_interfaces.rb

ActiveRecordInterface = GraphQL::InterfaceType.define do
  name "ActiveRecord"
  description "Active Record Interface"

  field :id, types.Int
  field :updated_at do
    type types.Int

    resolve -> (obj, args, ctx) {
      obj.updated_at.to_i
    }
  end
  field :created_at do
    type types.Int

    resolve -> (obj, args, ctx) {
      obj.created_at.to_i
    }
  end
end
# app/graph/types/user_type.rb

UserType = GraphQL::ObjectType.define do
  interfaces [ActiveRecordInterface]
  name "User"
  description "A user"

  field :email, types.String
end
# app/graph/types/post_type.rb

PostType = GraphQL::ObjectType.define do
  interfaces [ActiveRecordInterface]
  name "Post"
  description "A post"

  field :title, types.String
  field :content, types.String
end

Move business logic to Service object

CreatePostMutation = GraphQL::Relay::Mutation.define do
  # ...

  resolve -> (object, inputs, ctx) {
    Graph::CreatePostService.new(inputs, ctx).perform!
  }
end
CreatePostMutation = GraphQL::Relay::Mutation.define do
  # ...

  resolve -> (object, inputs, ctx) {
    post = ctx[:current_user].posts.create(title: inputs[:title], content: inputs[:content])

    {
      post: post
    }
  }
end

Use Relay module

GraphQL::Relay::ConnectionType
GraphQL::Relay::Node
GraphQL::Relay::Edge

Thanks :)