modern web development with

GraphQL

Israel Saeta Pérez

@dukebody

disclaimer

Outline

  • Motivation
  • GraphQL overview
  • GraphQL in Python (graphene)

What is graphql?

GraphQL is the specification of a query language and type system designed to solve the issue of under/over-fetching data in client-server web applications

traditional e-commerce app

Products

Reviews

Users

traditional e-commerce app

Product

id

name

description

detail

image

Review

id

product_id

stars

title

body

user_id

User

id

name

email

1

n

n

1

/products/
/products/<product-id>/reviews/
/users/

product listing

/products/

[
    {
        "id": 1,
        "name": "Topop linterna frontal ...",
        "description": "Linterna super brillante...",
        "detail": "Batería 3000 mAh, 2000 lúmenes..."
        "image": "https://images.amazon.es/....."
    },
    {
        ....
    }
]

Overfetching

overfetching tentative solutions

Different endpoint to get only the fields we need

/products-list/

[
    {
        "id": 1,
        "name": "Topop linterna frontal ...",
        "image": "https://images.amazon.es/....."
    },
    {
        ....
    }
]

Specify fields to include as querystring params

/products/?fields=id,name,image_url

product listing with stars

/products/

[
    {
        "id": 1,
        "name": "Topop linterna frontal ...",
        "description": "Linterna super brillante...",
        "detail": "Batería 3000 mAh, 2000 lúmenes...",
        "image": "https://images.amazon.es/....."
    },
    {
        ....
    }
]

Underfetching!

/products/<product-id>/reviews/  <-- n calls

/products-with-stars/

/products/?fields....,stars

Solutions

product reviews

/products/<product-id>/reviews/

[
    {
        "id": 1,
        "product_id": 2,
        "stars": 5,
        "title": "Awesome product..."
        "body": "This is the...",
        "user_id": 42,
    },
    {
        ....
    }
]

Underfetching!

We want user names, not ids

doesn't scale for complex apps

  • Multiple clients (mobile, API...)
  • Schema versioning (add & remove fields)
  • Backend-frontend coupling

What if we could use...

a richer query language?

  • Get what we need, no more, no less
  • No extra calls to fetch related data (- latency)
  • Facilitate schema versioning and reduce coupling

query language

(client, ~SQL select)

graphql

GraphQL is the specification of a query language and type system designed to solve the issue of under/over-fetching data in client-server web applications

What is graphql?

  • Created by Facebook in 2012, OSS in 2015
  • GitHub public API - Twitter and Giphy through hub
  • Reference implementation in JavaScript, but also implemented in Python, Ruby, PHP, Go... +12 langs!
  • Not coupled to any data store

list products with stars

/graphql/

# Query
query {
    products {
        id
        name
        image
        stars
    }
}

# Result
{
"data":
    "products": [
        {
            "id": 1,
            "name": "Topop linterna frontal ...",
            "image": "https://images.amazon.es/....."
            "stars": 5
        },
        {
            ....
        }
    ]
}

GET specific product with reviews

/graphql/

# Query
query {
  product(id:3) {
    id
    name
    image(size:"small")
    reviews {
      id
      stars
      title
      body
      user {
          id
          name
      }
    }
  }
}
# Result
{
  "data": {
    "product": {
      "id": 1,
      "name": "Topop linterna frontal ...",
      "image": "https://images.amazon.es/.....",
      "reviews": [
        {
          "id": 3,
          ...
          "user": {
            "id": 44,
            "name": "Rodolfo Langostino"
          }
        },
        ...
      ]
    }
  }
}
  • Arbitrary query depth
  • Arguments for any field

aliases

/graphql/

# Query
query {
  lantern: product(id:3) {
    id
    name
    description
  }
  coffee: product(id:2) {
    id
    name
    description
  }
}
# Result
{
  "data": {
    "lantern": {
      "id": 3,
      "name": "Topop linterna frontal ...",
      "description": "Linterna super guay..."
    },
    "coffee": {
      "id": 2,
      "name": "Super strong coffee...",
      "description": "Drink this coffee and fly..."
    }
  }
}

arbitrary names

fragments

/graphql/

# Query
query {
  lantern: product(id:3) {
    ...basicProductInfo
  }
  coffee: product(id:2) {
    ...basicProductInfo
  }
}

fragment basicProductInfo on Product {
  id
  name
  description
}
# Result
{
  "data": {
    "lantern": {
      "id": 3,
      "name": "Topop linterna frontal ...",
      "description": "Linterna super guay..."
    },
    "coffee": {
      "id": 2,
      "name": "Super strong coffee...",
      "description": "Drink this coffee and fly..."
    }
  }
}

variables

/graphql/

# Query
query getProductInfo($productId: Int!){
  product(id: $productId) {
    id
    name
    description
  }
}


# Separate (JSON) argument...
{
  "productId": 3
}
# Result
{
  "data": {
    "product": {
      "id": 3,
      "name": "Topop linterna frontal ...",
      "description": "Linterna super guay..."
    },
  }
}
  • No string concatenation --> less errors
  • Query validation
  • Pre-compilation

arbitrary

Directives

/graphql/

# Query
query getProductInfo(
    $productId: Int!, 
    $withReviews: Bool = true){
  product(id: $productId) {
    id
    name
    description
    reviews @include(if: $withReviews) {
      title
      stars
    }
  }
}


# Separate (JSON) argument...
{
  "productId": 3,
  "withReviews": false
}
# Result
{
  "data": {
    "product": {
      "id": 3,
      "name": "Topop linterna frontal ...",
      "description": "Linterna super guay..."
    },
  }
}
  • @include/@skip
  • Add more in near future?

type system

(server, ~SQL create)

graphql

type system

type Product {
  id: Int
  name: String
  description: String
  detail: String
  image(size: String="big"): String
  reviews: [Review]
}

type Review {
  id: Int
  product: Product
  product_id: Int
  user: User
  user_id: Int
  stars: Int
  title: String
  body: String
}

type User {
  id: Int
  name: String
  email: String
}

type Query {
  product(id: Int!): Product
  products: [Products]
  user(id: Int!): User
  users: [User]
}

Scalar types

  • Int, Float, String, Boolean, ID
  • Strongly typed
  • Required coercion
  • Query leaves

Object types

Interface types

Union of types

Introspection + validation + documentation

What we have covered so far...

  • How to query for what we want
  • How to define what is available

...but how do we implement the

the server fetching?

graphene

  • Python library for building APIs with GraphQL
  • Define types, resolution behaviour and execute queries
  • Integrations with Django, Flask and SQLAlchemy
  • Actively developed
  • Biggest issue right now: scarce documentation
  • Focused on Relay integration

types and resolution

type User {
  id: Int
  name: String
  email: String
}

type Query {
  user(id: Int!): User
  users: [User]
}
import graphene

class User(graphene.ObjectType):
    id = graphene.Int()
    name = graphene.String()
    email = graphene.String()


class Query(graphene.ObjectType):
    users = graphene.List(User)
    user = graphene.Field(User, id=graphene.Int())
    # product and products

    def resolve_users(self, args, context, info):
        users_data = data['users'].values()
        return [User(**u_data) for u_data in users_data]

    def resolve_user(self, args, context, info):
        p_data = data['users'][args['id']]
        return User(**p_data)

schema = graphene.Schema(query=Query)

dict with args coming from query

global helper store

(request, user, db connection)

root node

any object implementing declared interface

types and resolution

type Review {
  id: Int
  product: Product
  product_id: Int
  user: User
  user_id: Int
  stars: Int
  title: String
  body: String
}
import graphene

class Review(graphene.ObjectType):
    id = graphene.Int()
    product = graphene.Field(lambda: Product)
    product_id = graphene.Int()
    user_id = graphene.Int()
    user = graphene.Field(User)
    stars = graphene.Int()
    title = graphene.String()
    body = graphene.String()

    def resolve_user(self, args, context, info):
        return User(**data['users'][self.user_id])

    def resolve_product(self, args, context, info):
        return Product(**data['products'][self.product_id])
# Execute query
query = '''
query {
  products {
    name
  }
}
'''
response = schema.execute(query)
print(json.dumps(response.data))

Bind to django view

# urls.py

from graphene_django.views import GraphQLView

# ...

schema = graphene.Schema(query=Query)

urlpatterns = [
    url(r'^graphql', GraphQLView.as_view(schema=schema, graphiql=True)),
]

enable interactive in-browser IDE

django integration package

Don't Repeat Yourself

class Review(graphene.ObjectType):
    id = graphene.Int()
    product = graphene.Field(lambda: Product)
    product_id = graphene.Int()
    user_id = graphene.Int()
    user = graphene.Field(User)
    stars = graphene.Int()
    title = graphene.String()
    body = graphene.String()

very similar to Django model definition!

auto schema from models

# models.py

from django.db import models
from django.contrib.auth.models import User


class Product(models.Model):
    name = models.CharField(max_length=300)
    description = models.TextField()
    detail = models.TextField()
    image = models.CharField(max_length=300)


class Review(models.Model):
    product = models.ForeignKey(Product)
    user = models.ForeignKey(User)
    stars = models.IntegerField()
    title = models.CharField(max_length=300)
    body = models.TextField()
from django.contrib.auth.models import User
from graphene_django import DjangoObjectType
from .models import Product, Review


class ProductType(DjangoObjectType):
    class Meta:
        model = Product

    def resolve_review(self, args,
                       context, info):
        return self.review_set.all()


class ReviewType(DjangoObjectType):
    class Meta:
        model = Review


class UserType(DjangoObjectType):
    class Meta:
        model = User

needed for backrefs

if not using Relay

Main endpoints

# Query nodes for app
class Query(graphene.AbstractType):
    product = graphene.Field(ProductType, id=graphene.Int())
    products = DjangoListField(ProductType)
    user = graphene.Field(UserType, id=graphene.Int())
    users = DjangoListField(UserType)

    def resolve_product(self, args, context, info):
        return Product.objects.get(id=args['id'])

    def resolve_products(self, args, context, info):
        return Product.objects.all()

    def resolve_user(self, args, context, info):
        return User.objects.get(id=args['id'])

    def resolve_users(self, args, context, info):
        return User.objects.all()

What we have not covered

  • Mutations and InputTypes
  • Type-based fragments
  • Authorization
  • Relay integration

thanks!

# Query
mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
  createReview(episode: $ep, review: $review) {
    stars   # mutate + fetch review
    commentary
  }
}


# Variables JSON
{
  "ep": "JEDI",
  "review": {
    "stars": 5,
    "commentary": "This is a great movie!"
  }
}

Mutations

GraphQL

By Israel Saeta Pérez