martincartledge.github.io

Introduction to GraphQL

endpoint based APIs

revolves around HTTP

one size fits all

api that tries to answer too many use cases

Solution? Add more endpoints

let’s say we want to build an API that will allow us to fetch a products resource

browser GET /products

gaming console GET api/playstation/products

mobile GET api/mobile

the problem?

another approach: one endpoint per use case

browser GET api/products?version=browser

gaming console GET api/products?version=gaming

mobile GET api/products?version=mobile

or make it more generic via partials

GET api/products?partial=full

GET api/products?partial=minimal

allow clients to select what they want from the server

the JSON API spec calls these sparse fieldsets:

GET api/products?include=author&fields[products]=name,price

here is a query language in a query parameter (inspired by google drive’s API)

GET api/products?fields=name,photos(title, metadata/height)

Most of the above have tradeoffs that include optimization and customization

History

netflix has found substantial limitations in the traditional one size fits all REST API approach

while effective, the problem with the OSFA approach is that its emphasis is to make it convent for the API provider not for the API consumer

this solution involved a new conpcetual layer between the typical client and server layers where client-specific code if hosted on the server

in their approach

this allows the API to give control to client developers - letting them build their client adapters on the server

GraphQL is born

facebook’s data problem:

graphql is not:

graphql is:

graphql is a specification for an API query language and a server engine capable of executing such queries

hello world

this is a query that asks for the current user and their name


query {
  me {
    name
  }
}

me and name are referred to as fields

a client sends requests like these to a graphql server (usually as a simple string)

the response looks something like this

{
  "data": {
    "me": {
      "name": "martin"
    }
  }
}

notice that the response and the query are very similar shapes

if a graphql query is successful, the response always has a data key, under this key is the reponse that the client is expecting

graphql allows clients to define requirements down to single fields, this allows them to fetch exactly what they need

another example

query {
    me {
        name
        friends(first: 2) {
            name
            age
        }
    }
}

above we are fetching more than just a name, we are also fetching the name and the age of the first 2 of my friends - this is how we are able to traverse complex relationships. you will also notice that fields can take arguments. think of fields like functions, they can take arguments and return a certain type

the response looks something like this

{
    "data": {
      "me": {
        "name": "martin",
        "friends": [{
          "name": "nikita",
          "age": 3
        }, {
          "name": "willow",
          "age": 2
        }]
      }
    }
}

the query keyword is not a normal field. it tells the graphql server that we want to query off the query root of the schema

the type system

at the core of any graphql server is a powerful type system that helps to express API capabilities. the type system of a graphql engine is often referred to as the schema. a common way of representing a schema is through the graphql schema definition language (SDL)

the schema definition language is the canonical representation of a graphql schema and is well defined in the spec. no matter what manguage you are running a graohql api with, the SDL describes the final schema

example:

type Shop {
    name: String!
    # where the shop is location, null if online only
    location: Location
    products: [Product!]!
}

type Location {
    address: String
}

type Product {
    name: String!
    price: Price!
}

types and fields

the most basic and crucial primitive of a graphql schema is the object type. object types desribe one concept in your graphql API. what makes them whole is when they define fields. previously (above), we defined a Shop type that defined three fields: name, location, and products, the fieldName: Type syntax allows us to give a return type to our fields.

example: the name field on a Shop type returns a String. it is helpful to compare graphql fields to simple functions. fields are executed by a graphql server and return a value that maps correctly to its return type

the String type is not user-defined, it is part of graphql’s pre-defined scalar types. graphql’s real power life in the fact fields, which can return object types of their own

example

location: Location

the location field on the Shop returns a type Location which is a type that the schema defines. to see what fields are available on a Location type we can look at the Location type definition

type Location {
    address: String!
}

the Location type defines one address field which returns a String

now we will see that a field can return an object type of its own

a graphql server can execute queries like these because at each level in the query - it is able to validate the client requirements against the defined schema

query {
    # !. the shop field returns a `Shop` type
    shop(id: 1) {
        # 2. field location on the `Shop` type
        # Returns a `Location` type
        location {
            # 3. field address exists on the `Location` type
            # Returns a String
            address
        }
    }
}

if you will notice above, we know that our Shop type has a location field and that our Location type has an address field; however, where is the shop field coming from?

schema roots

a graphql schema must be defined using type and fields to describe its capabilities

a graphql schema must always define a Query Root (a type that defines the entry point to possibilities)

we usually call this type a Query

type Query {
    shop(id: ID): Shop!
}

the Query type is implicitly queried whenever you make a graphql api request

{
    shop(id: 1) {
        name
    }
}

this is valid because it implicitly asks for the shop field on the Query Root even though we did not query for that particular field that returned a Query type first

a Query Root has to be defined on a graphql schema, and there are two other types of roots that can be defined, a Mutation and a Subscription root

arguments

type Query {
    shop(id: ID!): Shop!
}

a graphql field can define arguments just like a function

the graphql server uses these arguments at the runtime resolution of the field

these fields are defined between parentheses after the field name and you can have as many of them as you like

type Query {
    shop(owner: String!, name: String!, location: Location): Shop!
}

arguments, like fields, can define a type which can either be a scalar type or an input type

input types are similar to types, but they are declared in a different way, using the input keyword

type Product {
    price(format: PriceFormat): Int!
}

input PriceFormat {
    displayCents: Boolean!
    currency: String!
}

variables

graphql queries can also define variables that can be used within a query

this allows clients to send variables along with a query and have the graphql server execute it instead of including it directly in the query string itself

query FetchProduct($id: ID!, $format: PriceFormat!) {
    product(id: $id) {
        price(format: $format) {
            name
        }
    }
}

we gave this query an operation name (FetchProduct)

a client would send this query with variables like this

{
  "id": "abc",
  "format": {
    "displayCents": true,
    "currency": "USD"
  }
}

aliases

the server dictates the canonical name of fields but if the client wants to receive fields under another name, they can use aliases

query {
    abcProduct: product(id: "abc") {
        name
        price
    }
}

above, the client requests the product field but defines an abcProduct alias

when the client executes the query, it will get back the field as if it was named abcProduct

{
  "data": {
    "abcProduct": {
      "name": "shirt",
      "price": 25
    }
  }
}

this is useful when requesting the same field multiples times with different arguments

mutations

allows writing and modifying data

the entry point to the mutations of a schema is under the Mutation root

to access the mutation root in a graphql query, use the mutation keyword at the top level of a query

mutation {
    addProduct(name: String!, price: Price!) {
        product {
            id
        }
    }
}

we define the addProduct mutation in a similiar way that we define fields on the query root

type Mutation {
    addProduct(name: String!, price: Price!): AddProductPayload 
}

type AddProductPayload {
    product: Product!
}

two things make mutation fields different from query fields

similarly they

enums

allow a schema to clearly define a set of values that may be returned, for fields, or passed (arguments)

they come in handy when defining an API that is easy to use by clients

type Shop {
    # the type of products the shop specializes in
    type: ShopType!
}

enum ShopType {
    APPAREL
    FOOD
    ELECTRONICS
}

abstract types

allow clients to expect the return type of a field to act a certain way, without returning an actual type

there are two ways to return an abstract type for fields, interfaces, and unions

Interfaces allow us to define a contract that a concrete type implementing it must answer to

interface Discountable {
    priceWithDiscounts: Price!
    priceWithoutDiscounts: Price!
}

type Product implements Discountable {
    name: String!
    priceWithDiscounts: Price!
    priceWithoutDiscounts: Price!
}

type GiftCard implements Discountable {
    code: String!
    priceWithDiscounts: Price!
    priceWithoutDiscounts: Price!
}

we have a Product type that implements a Discountable interface

this means that the Product type must define the two Discountable fields because by implementing the interface, it must respect the contract

this allows other fields to return Discountable directly - letting clients know they may request the fields part of that contract directly on the result (without knowing which concrete type will be returned at runtime)

we could have a discountedItems field that returns a list of either a Product or GiftCard type by directly returning an interface type of Discountable

type Cart {
    discountedItems: [Discountable!]!
}

all types are expected to answer to the Discountable contract; therefore, clients can directly ask for both of the price fields

query {
    cart {
        discountedItems {
            priceWithDiscounts
            priceWithoutDiscounts
        }
    }
}

if a client wants to query the other fields they must specify which concrete type they want to select against

you can use fragment spreads or typed fragments for this

query {
    cart {
        discountedItems {
            priceWithDiscounts
            priceWithoutDiscounts
            ...on Product {
                name
            }
            ... on GiftCard {
                code
            }
        }
    }
}

union types are slightly different

instead of defining a certain contract, union types are more of a bag of disparate objects that a field could return

you define them by using the union keyword

union CartItem = Product | GiftCard

type Cart {
    items: [CartItem]
}

it defines no contract, only the possible concrete type that could be returned by that field; therefore, clients have to specify the expected concrete type in all cases

query {
    cart {
        discountedItems {
            ... on Product {
                name
            }
            ... on GiftCard {
                code
            }
        }
    }
}

abstract types can be useful in graphql schemas; however, they can be easily abused

fragments

allow clients to define part of a query to be refused elsewhere

to create an inline fragment to select concrete types this syntax is used ... on Product

query {
    products(first: 100) {
        ...ProductFragment
    }
}

fragment ProductFragment on Product {
    name
    price
    variants
}

a fragment is defined by using the fragment keyword, it takes a name and a location where it can be applied, Product being the case shown above

directives

an annotation that can be used on various graphql primitives

the graphql specification defines two builtin directives that are very useful: @skip and @include

query MyQuery($shouldInclude: Boolean) {
    myField @include(if: $shouldInclude)
}

the @include directive ensures that the myField field is only queried when the variable shouldInclude is true

directives provide clients with a way to annotate fields in a way that can modify the execution behavior of a graphql server

directives can also accept arguments, like fields

you can also create custom directives (create a name and determine where the directive can be applied)

"""
Marks an element of a graphql schema as
only available with a feature flag activated
"""
directive @myDirective(
    """
    the identifier of the feature flag that toggles this field
    """
    flag: String
) on FIELD

the directive can be used by clients like this

query {
    user(id: "1") @myDirective {
        name
    }
}

besides, being applied to queries, directives can also be used with the type system directly

this makes them useful to annotate schemas with metadata

"""
marks an element of a graphql schema as
only available with a feature flag activated
"""
directive @featureFlagged(
    """
    the identifier of the feature flad that toggles this field
    """
    flag: String
) on OBJECT | FIELD_DEFINITION

then we can apply this directive to schema members directly

type SpecialType @featureFlagged(flag: "secret-flag") {
    secret: String!
}

wrapping up (introspection)

with graphql, clients can ask a graphql schema for what is possible to query

graphql schemas also include introspection meta fields which allows clients to fetch almost everything about its type system

query {
    __schema {
        types {
            name
        }
    }
}
{
  "data": {
    "__schema": {
      "types": [
        {
          "name": "Query"
        },
        {
          "name": "Product"
        },
      ]
    }
  }
}

this allows clients to discover use cases and it also enables amazing tooling

graphiql - an interactive graphql playground

introspection is used to generate code and to validate queries ahead of time

it is also used by IDEs to validate queries while developing an app

the graphql ecosystem is growing rapidly

in summary