GraphQL and Python







Òscar Vilaplana
@grimborg

PyGrunn 2017

self

Òscar Vilaplana


Software, Writing, Music

Python, Golang, NodeJS, React


@grimborg

http://oscarvilaplana.cat

Value

Our frontend engineers want us to use GraphQL!

―Harro van der Klauw, Engineer at Zupr

Example API: Friend's Plans

Friend's Plans

What Everyone is Up To

Friend's Plans

Simple API

# GET /friends

[{
	"id": "Aevae2wi",
	"name": "Wilbur Whateley",
	"avatar": "https://images.fakeurl.com/wilbur-whateley.png"
},
	...
]

# GET /plans?creator=Aevae2wi

[{
	"id": "huk8Phie",
	"creator": "Aevae2wi",
	"name": "Go check out weird color in forest",
	"date": "2017-06-17T11:48:25.333Z",
	"attendees": ["Aevae2wi", "Yah2sava", "thi2ooNg"],
	"location": "ooNahx7r"
},
	...
]

Friend's Plans

API Calls
# Fetch the upcoming plans
- GET /plans
# Fetch the creator names
- GET /friends/Aevae2wi
- GET /friends/Yah2sava
- GET /friends/thi2ooNg
- GET /friends/Aevae2wi
...
# Fetch the attendees names
- GET /friends/Xudou6Oo
- GET /friends/goe8Rae2
- GET /friends/eiphooR8
- GET /friends/iGee8kei
...
# Fetch the locations
- GET /locations/ooNahx7r
- GET /locations/tai0jeiF
...

REST

Pros
  • Easy to write (we'll see...)
  • Many tools/libraries available (we'll see...)

REST

Cons
  • Fetch too much / too little
  • Many calls
  • Documentation
  • Hard to discover
  • No usable smart generic client
  • Much postprocessing needed
  • Much to decide
  • Mismatch reality - API

Fixing...

# Embed the attendees
GET /friends/Aevae2wi/plans?include=attendees
# Only some fields
GET /friends/Aevae2wi/plans?include=attendees&fields=name,attendees
# Bulk fetch
GET /friends?id=Aevae2wi,Yah2sava,thi2ooNg,Aevae2wi

Friend's Plans

What's the data like?

Our frontend engineers want us to use GraphQL!

―Harro van der Klauw, Engineer at Zupr

because

they can just ask for what they want!

Inventing...

# Embed the attendees
GET /friends/Aevae2wi/plans?include=attendees
# Only some fields
GET /friends/Aevae2wi/plans?include=attendees&fields=name,attendees
# Bulk fetch
GET /friends?id=Aevae2wi,Yah2sava,thi2ooNg,Aevae2wi

You're trying to guess/decide what the client wants

Waste

Data

User's view

Data

Our view

Friend's Plans

What the client wants
  • Upcoming plans
  • Name of whose plan it is
  • Names of the people who are going
  • Address and map of where it will happen.

Friend's Plans

What response I would like

{
	plans {

		name
		description

		creator {
			name
		}

		location {
			name
			coordinates
		}

		attendees {
			name
		}

	}
}

Friend's Plans

What response I would like

{
	plans: [{

		name: "Go check out weird color at the forest"
		description: "It's freaking me out"

		creator: {
			name: "Wilbur Whateley"
		}

		location: {
			name: "Forest out of town"
			coordinates: "I don't know them"
		}

		attendees: [
			{ name: "Wilbur Whateley" }, { name: "Randolph Carter" }, ...
		]

	}, ...]
}

GraphQL

Basics
  • Query Language
  • Describe your data and how to operate with it (types, documentation, defaults...)
  • Client decides which data it needs.
  • Server resolves the data.

Queries

Types and Query Definitions

type Query {
	plans: [Plan]
}
		

type Plan {
	id: ID!
	name: String!
	description: String
	creator: Friend!
	attendees: [Friend]!
	location: Location!
}
		

type Friend {
	id: ID!
	name: String!
	avatarUrl: String
}
		

type Location {
	id: ID!
	name: String!
	coordinates: String!
}
		

Queries

Parameters

type Query {
	plans(limit: Integer, maxDays: Integer): [Plan]
}
		

{
	plans(limit: 10, maxDays: 7) {
		name
		creator {
			name
		}
	}
}
		

GraphQL

What's good
  • Client decides what it wants to receive
  • Single call
  • Self-documenting
  • Types: Validation

So, how does it work?

Where's the magic?

The magic is in 💕YOU💕🎉

GraphQL: Query Language

Implementation

Graphene Query

Simple resolver

import graphene

class Query(graphene.ObjectType):
    myself = graphene.String()

    def resolve_myself(self, args, context, info):
        return 'I am Groot'

schema = graphene.Schema(query=Query)
		

>>> result = schema.execute('''
  query {
    myself
  }
''')

>>> result.data
OrderedDict([('myself', 'I am Groot')])
		

Graphene Query

Fetch Data

import graphene

class Plan(graphene.ObjectType):
    name = graphene.String()
		...

class Query(graphene.ObjectType):
    plans = graphene.Field(Plan)

    def resolve_plans(self, args, context, info):
        return get_plans_from_db()

schema = graphene.Schema(query=Query)

Django

Graphene + Django

Your Models

class Friend(models.Model):
    name = models.CharField(max_length=200)

class Location(models.Model):
    name = models.CharField(max_length=200)

class Plan(models.Model):
    name = models.CharField(max_length=200)
    datetime = models.DateTimeField()
    description = models.CharField(max_length=1000)
    location = models.ForeignKey(Location)
    creator = models.ForeignKey(Friend)
    attendees = models.ManyToManyField('Friend', related_name="plan_attendees")

Graphene + Django

Schema from Models

import graphene
from graphene import Schema, resolve_only_args
from graphene_django import DjangoConnectionField, DjangoObjectType

from plans import models


class Friend(DjangoObjectType):

    class Meta:
        model = models.Friend

class Location(DjangoObjectType):

    class Meta:
        model = models.Location


class Plan(DjangoObjectType):
    attendees = graphene.List(Friend)

    @graphene.resolve_only_args
    def resolve_attendees(self):
        return self.attendees.all()

    class Meta:
        model = models.Plan

Graphene + Django

Mutations

class CreatePlan(graphene.Mutation):
    class Input:
        name = graphene.String(description='Name of the plan')
        description = graphene.String(description='Extended description of what the creator is up to')
        creator_id = graphene.String(description='ID of the friend who creates the plan')

    ok = graphene.Boolean()
    plan = graphene.Field(Plan)

    @staticmethod
    def mutate(root, args, context, info):
        try:
            creator = models.Friend.objects.get(id=args.get('creator_id'))
        except models.Plan.DoesNotExist:
            return CreatePlan(ok=False)

        plan = models.Plan(
            name=args.get('name'),
            description=args.get('description'),
            creator=creator
        )
        plan.save()
        return CreatePlan(plan=plan, ok=True)

class Mutation(graphene.ObjectType):
    create_plan = CreatePlan.Field()

Client

Queries

// queries.js

module.exports = {
	fetchItems: gql`
	query items {
		items {
			id
			name
		}
	}`,
	createItem: gql`
		mutation createItem($name: String!, $duration: Int!) {
			createItem(name: $name, duration: $duration) {
				id
			}
		}
	`
};

Client

Display data

class ItemList extends Component {
	render() {
		const { loading, error, items, refetch } = this.props.fetchItems;

		if (loading) {
			return <LoadingSpinner />
		}
		if (error) {
			return <ErrorPage />
		}
		return (
			<div>
				<ul>
					{items.map(item => <li><Item key={item.id} name={item.name} /></li>)}
				</ul>
				<Button onClick={refetch}>Refresh</Button>
			</div>
		)
	}
}

const ItemListWithData = graphql(queries.fetchItems)(ItemList);
```

Client

Modify data

class ItemCreator extends Component {
	handleSubmit = async ({ name }) => {
		const result = await mutate({
			variables: {
				name,
				duration,
			},
			refetchQueries: [
				{
					query: queries.fetchItems,
				},
			],
		});
	}
	// ...
}

const ItemCreatorWithMutation = graphql(queries.createItem)(ItemCreator);

Client

Modify data
  • Client has some understanding of the data
  • Client-side caching, smart fetching
  • Client knows when to refetch (sometimes it needs help)

Graphene + Django

How about...
  • Fetching many related entities?
    Batching / Dataloader
  • Auth, Rate / Depth Limiting?
    Use graphene-django middleware
  • Pagination
    Use Limit + Offset/Cursor (check out Relay)

tl;dr

  • GraphQL makes it easier to expose data
  • Closer to Data = Less Waste
  • Easy to get started: many resources (GraphQL, Graphene, Apollo, Relay)
  • Easy to allow your clients to kill your server: act preventively
  • Worth learning

Thanks!

@grimborg