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
- 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?
-
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 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 :)
Example Repo: https://github.com/wayne5540/graph_blog
Build a simple GraphQL API in Rails
By Wayne Chu
Build a simple GraphQL API in Rails
- 4,262