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
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
GraphQL
- 3,688