SANS

GERARD

Developer Evangelist

Developer Evangelist

International Speaker

Spoken at 166 events in 37 countries

Serverless Training

Serverless Training

GraphQL

Dan Schafer

Lee Byron

Nick Schrock

Co-Founders

GraphQL Server

source: blog

Query Lifecycle

SDL

GQL

Client

GraphQL Server

query

mutation

subscription

Components

Parse

request

Validate

GraphQL Client

Execute

Resolvers

response


// POST '/graphql'
{
  hello
}

// response.data
{
  "hello": "Hello world!"
}

GraphQL Query Language (GQL)

schema {
  query: Query
  mutation: Mutation
  subscription: Subscription
}

type Query {
  allTodos(first: Int): [Todo]
}

type Todo {  
  id: ID!
  text: String!
  complete: Boolean!
}

Types

Field 

Field 

Field 

Arguments

Mandatory

List

Types

Types

This is optional

Schema Definition Language (SDL)

GraphQL Community

Why Developers use GraphQL?

  • Best tooling for data access
  • De-coupled from storage
  • Safe: validated and structured
  • Collaborative by design
  • Supafast for Web and Mobile!

So what is GraphQL secret superpower then?

Tooling?

Introspection?

None of those...

The secret superpower is...

Extensibility

GraphQL Directives

GraphQL Directives

  • Annotations to extend your GraphQL API by adding meta-data or custom logic
  • Available in the client via Queries (GQL) or the server via the Schema (SDL)

What can you do with Directives?

Analytics

Performance

background Layer 1

Logging

background Layer 1

Default values

background Layer 1

Validation

Caching

Authorisation

Data Formatting

background Layer 1

Step 1: Add the directive to the Schema

Creating a Directive

directive @specifiedBy(
  url: String!                                   
) on SCALAR 

Identifier

Argument

Location

Use repeatable modifier to apply more than once!

scalar UUID

Creating a Directive

Step 2: Annotate

scalar UUID @specifiedBy(
  url: "https://bit.ly/rfc412"
)

Creating a Directive

Step 2: Annotate

Client: 8 extension locations

query getUser($var: ID @variable) @query {
  me {
    id @field
    ... UserDetails @fragment_spread
    ... on User @inline_fragment { ... }
  }
}
fragment UserDetails on User @fragment_definition { ... }

mutation AddVote @mutation { ... }
subscription VotesSubscription @subscription { ... }

Server: 11 extension locations

schema @schema { ... }
type Query @object {
  getUser(id: ID! @argument):User @field_definition 
}
input BlogPostContent @input_object {
  title: String @input_field_definition 
}
enum MediaFormat @enum {
  VIDEO @enum_value
}
union SearchResult @union = Book | Movie
interface Book @interface { ... }
scalar DateTime @scalar

Native Directives

Native GraphQL Directives

Query

GQL

@include

@skip

Client

Native GraphQL Directives

Query

GQL

@include

@skip

Client

Schema

SDL

@deprecated

@specifiedBy

Server

Definition for @skip

directive @skip(
  if: Boolean!
) on FIELD | INLINE_FRAGMENT | FRAGMENT_SPREAD

@skip in fields

directive @skip(
  if: Boolean!
) on FIELD | INLINE_FRAGMENT | FRAGMENT_SPREAD

query ($anonymous: Boolean!) {
  me {
    name @skip(if: $anonymous)
  }
}

@skip in inline fragments

directive @skip(
  if: Boolean!
) on FIELD | INLINE_FRAGMENT | FRAGMENT_SPREAD

query ($anonymous: Boolean!) {
  me {
    ...on User @skip(if: $anonymous) {
      name 
    }
  }
}

@skip in fragments

directive @skip(
  if: Boolean!
) on FIELD | INLINE_FRAGMENT | FRAGMENT_SPREAD

query ($anonymous: Boolean!) {
  me {
    ...UserDetails @skip(if: $anonymous)
  }
}
fragment UserDetails on User {
  name
}

Definition for @deprecated

directive @deprecated(
  reason: String = "No longer supported"
) on FIELD_DEFINITION | ENUM_VALUE

@deprecated in fields

directive @deprecated(
  reason: String = "No longer supported"
) on FIELD_DEFINITION | ENUM_VALUE

type User {
  uuid: String @deprecated
}

Reason is optional and accepts Markdown! Cool!

@deprecated in Enums

directive @deprecated(
  reason: String = "No longer supported"
) on FIELD_DEFINITION | ENUM_VALUE

enum TaskStateEnum {
  IN_PROGRESS @deprecated(reason:"Obsolete")
}

Oh lord!   This message shows in the Docs

Better UX with @defer

Faster with @defer

without @defer

request

fast

slow fragment

response

with @defer

request

fast

response

slow fragment

patch

time

Initial load

Update

500ms

700ms

850ms

Initial load

@defer (experimental) 

  • Improve User Experience
  • Postpone non-essential data
  • Automatically split results for faster response times via HTTP-Multipart patches
  • Minimise impact of slow or large fragments
  • Fragments can be of one or more fields

Definition for @defer

directive @defer(
  if: Boolean = true, label: String                                            
) on FRAGMENT_SPREAD

Attention! Labels must be unique across queries

Fixing slow queries

type Query {
  getUserById(id: ID!): User                                     
}
type User {
  id: ID!
  name: String!
  performance: EmployeFile 
}
type EmployeFile {
  review: String
}

Identify the root cause

query loadUser($id: ID!) {
  getUserById(id: $id) {
    name
    performance {
      review                                             
    }
  }                                     
}

Geez! This query is really SLOWWW 🤔 

Improving slow fields

query loadUser($id: ID!) {
  getUserById(id: $id) {
    name
    ...EmployeeReview
  }                                     
}
fragment EmployeeReview on User {
  performance {
    review
  }
}

Oh! I see…

Example Request

query loadUser($id: ID!) {
  getUserById(id: $id) {
    name
    ...EmployeeReview @defer(label:"EmployeeReviewDefer")
  }                                     
}
fragment EmployeeReview on User {
  performance {
    review
  }
}

Wait what is @defer?

Initial Response

{
  "data": {
    "getUserById": {
      "name": "Elon Musk",                                      
    }
  },
  "hasNext": true
}

time

fast

slow fragment

response

patch

request

Patch Response

{
  "label": "EmployeeReviewDefer"
  "path": ["getUserById"]
  "data": {
    "performance": {
      "review": "Exceeds expectations",                                                          
    }
  },
  "hasNext": false
}

time

fast

slow fragment

response

patch

request

Client Code for React

time

fast

slow fragment

response

patch

request

<Query query={query} errorPolicy="all">{
 ({ loading, error, data, loadingState }) => {
  if (loading) return <Loader />;
  if (error) return <RawResponse error={error} />;
  return (
    <Employee name={data.getUserById} />
    <EmployeeReview performance={data.performance}
      loadingState={loadingState && loadingState.performance}     
    />)
  }
}
</Query>

Jeff Bezos

Exceeds expectations

Custom Directives

api.chucknorris.io

{
  "id": "dnKbjh3RSfGXfcp5Rwiy9Q",
  "value": "The Grand Canyon was caused by Chuck Norris pissing there--Once.",
  "created_at": "2020-01-05 13:42:19.897976",
  "updated_at": "2020-01-05 13:42:19.897976",
  "url": "https://.../dnKbjh3RSfGXfcp5Rwiy9Q",
  "icon_url": "https://.../chuck-norris.png",
  "categories": [],
}

api.chucknorris.io JSON response 

directive @rest(
  url: String!
  property: String!
) on FIELD_DEFINITION

type Query {
  hello: String @rest(
   url: "https://api.chucknorris.io/jokes/random",             
   property: "value"
  )                 
}

Custom Directive: @rest

function restDirectiveTransformer(schema) {
  return mapSchema(schema, {
    [MapperKind.OBJECT_FIELD]: fieldConfig => {
      const restDirective = getDirective(schema, fieldConfig, "rest")?.[0];
      if (restDirective) {
        const { url, property } = restDirective;
        fieldConfig.resolve = async () => {
          const response = await (await fetch(url)).json();
          return response[property];
        };
        return fieldConfig;
      }
    }
  });
}

Custom Directive: @rest

type Query {
  hello: String @rest(
   url: "https://api.chucknorris.io/jokes/random",             
   property: "value"
  )                 
}

Custom Directive: @rest

directive @uppercase on FIELD_DEFINITION

type Query {
  hello: String @uppercase                  
}

Custom Directive: @uppercase

function upperDirectiveTransformer(schema, directiveName) {
  return mapSchema(schema, {
    [MapperKind.OBJECT_FIELD]: fieldConfig => {
      const upperDirective = getDirective(schema, fieldConfig, directiveName)?.[0];      
      if (upperDirective) {
        const { resolve = defaultFieldResolver } = fieldConfig;
        fieldConfig.resolve = async function (source, args, context, info) {
          const result = await resolve(source, args, context, info);
          return result.toUpperCase();
        }
        return fieldConfig;
      }
    }
  });
}

Custom Directive: @uppercase

type Query {
  hello: String @uppercase                  
}

Custom Directive: @uppercase

 Unleash

GraphQL secret superpower!

How are you going to use it?

More

ardatan/graphql-tools

contra/graphql-helix

GraphQLs secret superpower

By Gerard Sans

GraphQLs secret superpower

In this talk, we’ll explore a secret API design pattern: GraphQL directives and how to use them without breaking a sweat. Best of all, you will be able to add new features to your schema without having to change your existing code. A fantastic technique to add to your GraphQL toolset!

  • 1,774