Give REST a rest,

use GraphQL for your next Ruby API

@eshaiju

About me

I am from Malappuram, Kerala

Technology stack - Rails + GraphQL+ ReactJS +Apollo

A simple UI component

A simple UI component

Comment

Post

User

What kind of data is needed? ​

Reusable endpoints (REST)

GET /comments/post_id=1

GET /comments/post_id=2

GET /comments/post_id=5

GET /posts

GET /users/1
GET /users/2
GET /users/5

Too many round trips!

Still waiting..............

"post": {
  "id": 1,
  "title": "A GraphQL Server Implementation",
  "url": "http://tech.eshaiju.in/blog/2017/05/06/a-graphql-server-implementation-ruby-on-rails/",
  "body": null,
  "votes": 100,
  "published_at": "11 Jul 2017 00 00 AM",
  "auther_name": "Shaiju E",
  "created_at": "2017-07-11 12:57:20 UTC",
  "updated_at": "2017-07-11 13:15:50 UTC"
}
"post": {
  "id": 1,
  "title": "A GraphQL Server Implementation",
  "url": "http://tech.eshaiju.in/blog/2017/05/06/a-graphql-server-implementation-ruby-on-rails/"
}

What I get

What I care about

  Over-fetching of data

 Custom Endpoints

/posts_with_everything_i_need
/posts_with_everything_i_need
/posts_with_everything_i_need_with_author_v2
/posts_with_everything_i_need_with_author_and_images
/posts_with_everything_i_need_with_author_and_images_for_mobile
/tightly_coupled_endpoint_for_a_specific_client_bla_bla_bla_bla
/posts_with_everything_i_need_with_author_and_images_for_tab_v2

 

Client

 

Server

Updates a view

Update endpoints

Creates a new view

New view version

Create new endpoint

GraphQL

What GraphQL is NOT

What GraphQL is NOT

It's not about graph databases and graph search

It's not about data storage

it's not language specific

it's not a library

What GraphQL IS

What GraphQL IS

GraphQL is a query language for APIs

GraphQL is a new API standard that provides an alternative to REST

Declarative data fetching - The client specifies what data shapes, and the server returns that data in exact same shape.

GraphQL server only exposes a single endpoint and responds with precisely the data a client asked for

query {
  post(id: 1){
    title
    url
    published_at
  }
}

Query

query {
  post(id: 1){
    title
    url
    published_at
  }
}
{
  "data": {
    "post": {
      "title": "A GraphQL Server Implementation",
      "url": "http://tech.eshaiju.in/blog/2017/05/06/a-graphql-server-implementation-ruby-on-rails/",
      "published_at": "2017-07-11 00:00:00 UTC"
    }
  }
}
query {
  users{
    id
    name
  }
  post(id: 1){
    title
    url
    published_at
    author{
      name
    }
    comments{
      body
      author{
        name
      }
    }
  }
}
{
  "data": {
    "users": [
      {
        "id": 1,
        "name": "Shaiju E"
      },
      {
        "id": 2,
        "name": "Awin"
      }
    ],
    "post": {
      "title": "A GraphQL Server Implementation",
      "url": "http://tech.eshaiju.in/blog/2017/05/06/a-graphql-server-implementation-ruby-on-rails/",
      "published_at": "11 Jul 2017 00 00 AM",
      "author": {
        "name": "Shaiju E"
      },
      "comments": [
        {
          "body": "Good post",
          "author": {
            "name": "Awin"
          }
        }
      ]
    }
  }
}

Mutations are the equivalent to the POST, PUT, PATCH and DELETE in HTTP/REST.

mutation addComment{
  addComment(input: { comment: "New comment", post_id: 1, author_id: 1})
  {
    post{
      id
      title
      comments{
        body
        user{
          name
        }
      }
    }
  }
}

Mutations are used to change your data

Mutation

Type System

query {
  users{
    id
    name
  }
  post(id: 1){
    title
    url
    published_at
    author{
      name
    }
    comments{
      body
      author{
        name
      }
    }
  }
}
QueryType = GraphQL::ObjectType.define do
  name "Query"
  description "The query root of this schema"

  field :post do
    type PostType
    description "Find a Post by ID"

    argument :id, !types.ID
    resolve ->(obj, args, ctx) { Post.find_by_id(args[:id]) }
  end

  field :posts, types[PostType] do
    description "Return all posts"

    resolve ->(obj, args, ctx) { Post.all }
  end
  
  field :users, types[UserType] do
    description "Fetch all users"

    resolve ->(obj, args, ctx) { User.all }
  end
end
PostType = GraphQL::ObjectType.define do
  name "Post"
  description "Post and details"

  field :id, types.Int
  field :title, types.String do 
    resolve -> (post, args, ctx) { post.title }
  end
  field :body, types.String
  field :url, types.String
  field :votes, types.Int
  field :published_at, types.String do
    resolve -> (post, args, ctx) { 
     post.published_at.strftime('%d %b %Y %H %M %p') 
    }
  end
  field :comments, types[CommentType]
  field :author, UserType
end
CommentType = GraphQL::ObjectType.define do
  name "Comment"
  field :id, types.Int
  field :body, types.String
  field :author, UserType
  field :post, PostType
end


UserType = GraphQL::ObjectType.define do
  name "User"
  field :id, types.Int
  field :name, types.String
  field :joined_at
end
HackerNewsSchema = GraphQL::Schema.define do
  query QueryType
  mutation MutationType
end

Schema

class GraphqlController < ApplicationController
  def execute
    variables = ensure_hash(params[:variables])
    query = params[:query]
    context = {
      # Query context goes here, for example:
      current_user: current_user,
    }
    result = HackerNewsSchema.execute(
                                        query,
                                        variables: variables,
                                        context: context
                                      )
    render json: result
  end
end

POST /graphql

curl -XPOST -d 'query=
{ 
  post(id: 1)
  { 
    title 
    url 
  }
}' http://localhost:3000/graphql

API Documentation

Drawbacks

and solutions

N+1 Queries

{
  posts{
    title
    url
    comments{
      body
      author {
        name
      }
    }
  }
}

Solution: Batching + Caching

CommentType = GraphQL::ObjectType.define do
  name "Comment"
  field :id, types.Int
  field :body, types.String
  field :author, UserType do
    resolve -> (obj, args, ctx) {
      RecordLoader.for(User).load(obj.author_id)
    }
  end
end

HTTP Caching

Solution: Client Side Cache

Security

malicious Query

query {
  posts{
    title
    url
    author{
      name
      comments{
        author{
          comments{
            author
            {
              name
              comments{
                author{
                  name
                  comments{
                    author{
                      name
                      comments{
                        body
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

Timeouts

Solutions

HackerNewsSchema.middleware << GraphQL::Schema::TimeoutMiddleware.new(max_seconds: 10) do |err, query|
  Rails.logger.info("GraphQL Timeout: #{query.query_string}")
end

Query Depth & Query Complexity

MySchema = GraphQL::Schema.define do
 # ...
 max_depth 12
 max_complexity 100
end

Solutions

Client

Server

Hey, give me the resource with id = 1

OK, here’s the resource with id = 1 

In REST System

Client

Server

Hey, give me the resource with id = 2

OK, here’s the resource with id = 2 

In REST System

Client

Server

Here is everything you can do! (capabilities)

I need that exact data shape (requirements)

GraphQL

OK, here’s the data with same shape

Who’s using GraphQL?

Who’s using GraphQL?

Advantages

Efficient

Predictable

Flexible

Auto Documentation

Thank you!

graphql-deccanrubyconf

By Shaiju Nonu

graphql-deccanrubyconf

  • 118

More from Shaiju Nonu