@timrossinfo
(Always start with a problem)
Slow to construct a response
High memory usage on server
Large response payload for mobile clients
Providing data for multiple contexts
3x faster response time
Reduced memory usage on server
Initial response payload < 10kb
Simplified code on client and server
Requires multiple requests, following links to other resources, or a deeply nested object graph
Returns additional data to handle different contexts, or use multiple contextual endpoints
API Discoverability
GraphQL provides a way for developers to specify the precise data needed for a view and enables a client to fetch that data in a single network request.
Initially developed internally by Facebook in 2012 before being released publicly in 2015. Now hosted by the non-profit Linux Foundation.
{
"data": {
"listing": {
"title": "Japanese Zen Retreat"
}
}
}
type Query {
listing(id: ID!): Listing
}
query {
listing(id: 674) {
title
}
}
type Listing {
id: ID!
title: String!
description: String!
photos: [Photo!]
}
type Photo {
id: ID!
image: PhotoImage!
position: Int
}
type PhotoImage {
hero: String!
large: String!
medium: String!
original: String!
thumb: String!
}
An in-browser IDE for exploring GraphQL
module Types
class ListingType < Types::BaseObject
field :id, ID, null: false
field :title, String, null: false
field :description, String, null: false
field :photos, [PhotoType], null: true
def photos
AssociationLoader.for(Listing, :photos).load(object)
end
end
end
module Types
class ListingStatesType < BaseEnum
value 'NSW', 'New South Wales'
value 'VIC', 'Victoria'
end
end
module Types
module LookupType
include Types::BaseInterface
field :name, String, null: false
field :slug, String, null: false
end
end
module Types
class AmenityType < Types::BaseObject
implements Types::LookupType
end
end
module Types
class SearchResultType < Types::BaseUnion
possible_types Types::ListingType, Types::StoryType
def self.resolve_type(object, _context)
if object.is_a?(Story)
Types::StoryType
else
Types::ListingType
end
end
end
end
{
search(params: {
type: accommodation,
state: VIC
}) {
results {
__typename
... on Story {
featuredListing {
title
}
}
... on Listing {
subcategory {
name
}
}
}
}
}
Visibility
Hide parts of the schema from some users
Accessibility
Reject certain queries from unauthorized users
Authorization
Check if current user has permission to access object
Scoping
Filter a list of objects appropriate for the current user or context
module Types
class AdminFeature < Types::BaseObject
def self.visible?(context)
# only show fields with this type to admin users
super && context[:current_user].admin?
end
end
end
field :abn_lookup, ABNLookupType, null: true do
argument :abn, String, required: true
def accessible?(context)
super && context[:current_user]
end
end
This is different from visibility, where unauthorized parts of the schema are treated as non-existent. It’s also different from authorization, which makes checks while running, instead of before running.
DEPRECATED
module Types
class Conversation < Types::BaseObject
# You can only see the details of a `Conversation`
# if you're one of the people involved in it.
def self.authorized?(object, context)
super && object.participants.include?(
context[:current_user]
)
end
end
end
While a query is running, you can check each object to see whether the current user is authorized to interact with that object.
module Types
class Conversation < Types::BaseObject
def self.scope_items(items, context)
items.where(participant: context[:current_user])
end
end
end
module Types
class User < Types::BaseObject
field :conversations, [Conversation], scope: true
end
end
Rather than checking “can this user see this thing?”, scoping filters items appropriate for the current user and context
module Types
class QueryType < Types::BaseObject
field :conversation, Conversation, null: false do
argument :id, ID, required: true
end
def conversation(:id)
# Find conversation through current user
context[:current_user].conversations.find(id)
end
end
end
My preference is to always access records through the scope of the current user
module Types
class StoryType < Types::BaseObject
field :featured_listing, ListingType, null: false
def featured_listing
RecordLoader.for(Listing).load(object.listing_id)
end
end
end
query {
stories {
id
featured_listing {
title
}
}
}
No N+1
module Types
class MutationType < Types::BaseObject
field :create_booking, mutation: Mutations::CreateBooking
end
end
module Types
class BookingInputType < Types::BaseInputObject
argument :start_date, Types::DateType, required: true
argument :end_date, Types::DateType, required: true
argument :adults, Int, required: true
argument :children, Int, required: true
end
end
module Mutations
class CreateBooking < BaseMutation
argument :listing_id, ID, required: true
argument :booking_input, Types::BookingInputType, required: true
field :booking, Types::BookingType, null: true
def resolve(listing_id:, booking_input:)
listing = Listing.active.find(listing_id)
booking = ::Listings::CreateBooking.call(
listing, booking_input.to_h, context[:current_user]
)
{
booking: booking
}
end
def ready?(**_args)
return true if context[:current_user].present?
raise GraphQL::ExecutionError, 'Login required'
end
end
end
mutation {
createBooking(
listingId: 490,
bookingInput: {
startDate: "2019-10-01",
endDate: "2019-10-03",
adults: 2,
children:0
},
) {
booking {
id
startDate
endDate
}
}
}
{
"data": {
"createBooking": {
"booking": {
"id": "18533",
"startDate": "2019-10-01",
"endDate": "2019-10-03"
}
}
}
}
GraphQL::Errors.configure(RiparideSchema) do
rescue_from ActiveRecord::RecordNotFound do
nil
end
rescue_from ActiveRecord::RecordInvalid do |exception|
GraphQL::ExecutionError.new(
exception.record.errors.full_messages.join("\n")
)
end
rescue_from UnprocessableEntity do |exception|
GraphQL::ExecutionError.new(exception.message)
end
end
import gql from 'graphql-tag'
import { useQuery } from '@apollo/react-hooks'
const GET_LISTINGS = gql`
{
listings {
id
title
}
}
`
const Listings = () => {
const { loading, error, data } = useQuery(GET_LISTINGS)
if (loading) return 'Loading...'
return (
<select name="listing">
{data.listings.map(listing => (
<option key={listing.id} value={listing.id}>
{listing.title}
</option>
))}
</select>
)
}
React Hook
import React, { useState } from 'react'
const Example = () => {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0)
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
)
}
Hooks let you use state and other React features without writing a class
import React, { useState, useEffect } from 'react'
const Example = () => {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0)
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`
})
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
)
}
import React, { useMutation } from 'react'
const Example = ({ listingId, startDate, endDate, adults, children }) => {
const [createBooking, { loading, data }] = useMutation(CREATE_BOOKING,
variables: {
listingId,
bookingInput: {
startDate,
endDate,
adults,
children
}
}
)
return (
<div>
<button onClick={createBooking} disabled={loading}>
Create Booking
</button
</div>
)
}
useMutation hook
fragment NameParts on User {
firstName
lastName
}
query GetUser {
user(id: "7") {
...NameParts
avatar(size: LARGE)
}
}
A GraphQL fragment is a shared piece of query logic.
Gradually migrate an existing API
Continue to use REST for simple APIs
Carefully consider authorization
GraphQL Official Site
https://graphql.org/
GraphQL Ruby
https://graphql-ruby.org/
Apollo Client
https://www.apollographql.com/docs/react/