Wayne Chu
ID: wayne5540
wayne.5540@gmail.com
Korea & Japan
China
Philippines
Taiwan
https://github.com/wayne5540/graph_blog
http://bit.ly/graphqlroro
Github released their GraphQL API Alpha at Sep-2016
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
I can get many resources in a single request
Customise response to what I want
As Frontend
As Backend
Versionless API
No API documentation
Introspection system
## 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
As an User...
[ ] I need an account so I can use API.
[ ] I can get my account information from 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
As an User...
[V] I need an account so I can use 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
# 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
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
# 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}" }
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
# 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
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
# 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
# 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
# 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)
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
# config/application.rb
module GraphBlog
class Application < Rails::Application
# ...
config.autoload_paths << Rails.root.join('app', 'graph', 'types')
end
end
# 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
As an User...
[V] I need an account so I can use API.
[V] I can get my account information from API.
query {
viewer {
id
email
}
}
query {
me: viewer {
...userFields
}
alsoIsMe: viewer {
...userFields
}
}
fragment userFields on User {
user_id: id
email
}
Example
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)
#
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
# 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/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
# 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
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.
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
}
}
}
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
# 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
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
GraphQL::Relay::ConnectionType
GraphQL::Relay::Node
GraphQL::Relay::Edge
Example Repo: https://github.com/wayne5540/graph_blog