Slide 1

Slide 1 text

Better APIs using GraphQL with Python and React Mike Jones @imsickofmaps

Slide 2

Slide 2 text

We help employers protect and improve the financial well‑being of their employees through innovative and impactful financial products.

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

WHY GRAPHQL? what’s wrong with REST? is this JS-fanboi NIH syndrome? I’m not Facebook Isn’t this just front-end stuff?

Slide 5

Slide 5 text

WHAT PROBLEM DOES IT SOLVE? /api/clients/with_only_important /api/clients?fields=title,office Too many fat requests Smoother inter-team releases

Slide 6

Slide 6 text

source: http://atomicdesign.bradfrost.com/chapter-2/ WHAT PROBLEM DOES IT SOLVE?

Slide 7

Slide 7 text

WHAT IS IT NOT? Installable Language Specific A silver bullet Replacement for REST

Slide 8

Slide 8 text

WHAT IS IT? Query Langage Nested Resources Specification for servers Self-documenting Typed

Slide 9

Slide 9 text

STOP TALKING AND SHOW ME

Slide 10

Slide 10 text

query { me { firstName } } query { user(id: 1) { firstName } } { "data": { "me": { "firstName": "Mike" } } } { "data": { "user": { "firstName": "Mike" } } }

Slide 11

Slide 11 text

http://graphql.org/learn/queries/

Slide 12

Slide 12 text

mutation { addUser(input: {firstName: “Dave”}) { user { firstName } } } { "data": { "addUser": { "user": { "firstName": "Dave" } } } }

Slide 13

Slide 13 text

GETTING STARTED This is NOT a Django/SQLAlchemy/AppEngine tutorial! 1. Install graphene-django 2. Create your data models 3. Create a schema for app 4. Create a mutation for app 5. Create a root project schema 6. Add URL entry 7. Add config settings 8. PROFIT / RETIRE / *cough* write tests

Slide 14

Slide 14 text

print 'hello world!' 1. Install graphene-django $ pyenv virtualenv 3.6.1 proj $ pyenv shell proj $ pip install graphene-django

Slide 15

Slide 15 text

print 'hello world!' 2. Create data models from django.db import models class UserModel(models.Model): name = models.CharField(max_length=100) last_name = models.CharField(max_length=100) app/models.py

Slide 16

Slide 16 text

print 'hello world!' 3. Create schema for app from graphene_django import DjangoObjectType import graphene class User(DjangoObjectType): class Meta: model = UserModel class Query(graphene.ObjectType): users = graphene.List(User) @graphene.resolve_only_args def resolve_users(self): return UserModel.objects.all() app/schema.py

Slide 17

Slide 17 text

print 'hello world!' 4. Create mutation for app from graphene import AbstractType, relay, String class UserMutation(relay.ClientIDMutation): class Input: name = String() last_name = String() user = Field(UserNode) @classmethod def mutate_and_get_payload(cls, input, context, info): name = input.get('name', None) last_name = input.get('last_name', None) # Create the model user = UserModel.objects.create(name=name, last_name=last_name) return UserMutation(user=user) class Mutation(AbstractType): user_mutate = UserMutation.Field() app/schema.py

Slide 18

Slide 18 text

print 'hello world!' 5. Create root project schema import app.schema import graphene class Query(app.schema.Query, graphene.ObjectType): pass class Mutation(app.schema.Mutation, graphene.ObjectType): pass proj/schema.py

Slide 19

Slide 19 text

print 'hello world!' 6. Add URL entry from django.conf.urls import url from graphene_django.views import GraphQLView urlpatterns = [ # ... url(r'^graphql', GraphQLView.as_view(graphiql=True)), ] proj/urls.py

Slide 20

Slide 20 text

print 'hello world!' 7. Add config settings INSTALLED_APPS = ( # ... 'graphene_django', ) GRAPHENE = { 'SCHEMA': 'proj.schema.schema' proj/settings.py

Slide 21

Slide 21 text

print 'hello world!' 8. PROFIT! $ ./manage.py runserver $ open http://127.0.0.1:8000/graphql

Slide 22

Slide 22 text

MAKING IT BETTER 1. Allow edits 2. Install django-filter and add some filters 3. Add some security 4. Add custom resolvers 5. Test

Slide 23

Slide 23 text

print 'hello world!' 1. Allow edits @classmethod def mutate_and_get_payload(cls, input, context, info): mutation_data = { "name": input.get('name', None), "last_name": input.get('last_name', None) } if "id" in input: # lookup existing id = uuid.UUID(str(base64.b64decode( input.get("id")).split(b":")[1], "utf-8")) try: user = UserModel.objects.get(id=id) except UserModel.DoesNotExist: return UserMutation(user=None) for field, value in mutation_data.items(): setattr(user, field, value) user.save() else: # create new user = UserModel.objects.create(**mutation_data) return UserMutation(user=user) app/schema.py

Slide 24

Slide 24 text

print 'hello world!' 2. Install django-filter and add some filters import django_filters from graphene_django.filter import DjangoFilterConnectionField class UserFilter(django_filters.FilterSet): class Meta: model = UserModel fields = { 'somefk__id': ['exact'], 'name': ['exact', 'icontains', 'istartswith'], } order_by = OrderingFilter(fields=['last_name', ‘name']) class Query(AbstractType): user = relay.Node.Field(UserNode) users = DjangoFilterConnectionField(UserNode, filterset_class=UserFilter) app/schema.py

Slide 25

Slide 25 text

print 'hello world!' 3. Add some security class UserNode(DjangoObjectType): class Meta: model = UserModel exclude_fields = ('password', ) interfaces = (relay.Node, ) @classmethod def get_node(cls, id, context, info): try: node = cls._meta.model.objects.get(id=id) except cls._meta.model.DoesNotExist: return cls._meta.model.objects.none() if context is not None: # HTTP request if context.user.is_authenticated and ( has_object_permission('access_user', context.user, node)): return node else: return cls._meta.model.objects.none() else: # Not a HTTP request - no permissions testing currently return node app/schema.py $ pip install django-role-permissions

Slide 26

Slide 26 text

print 'hello world!' 3. Add some security cont… from django.views.decorators.csrf import csrf_exempt from django.contrib.admin.views.decorators import staff_member_required from rest_framework.authtoken import views from rest_framework.authentication import TokenAuthentication from rest_framework.permissions import IsAuthenticated from rest_framework.decorators import authentication_classes, permission_classes, api_view from graphene_django.views import GraphQLView def graphql_token_view(): view = GraphQLView.as_view(schema=schema) view = permission_classes((IsAuthenticated,))(view) view = authentication_classes((TokenAuthentication,))(view) view = api_view(['POST'])(view) return view urlpatterns = [ url(r'^graphql', graphql_token_view()), url(r'^graphiql', staff_member_required(csrf_exempt(GraphQLView.as_view(graphiql=True)))), url(r'^api/auth/', include('rest_framework.urls', namespace='rest_framework')), url(r'^api/token-auth/', views.obtain_auth_token), ] proj/urls.py $ pip install django-rest-framework

Slide 27

Slide 27 text

print 'hello world!' 4. Add custom resolvers class Query(AbstractType): me = Field(IdentityNode) def resolve_me(self, args, context, info): # context will reference to the Django request if context is not None: if context.user.is_authenticated: return UserModel.objects.get(user=context.user) else: return UserModel.objects.none() else: # Not a HTTP request - no user checking currently return UserModel.objects.none() app/schema.py

Slide 28

Slide 28 text

print 'hello world!' 5. Test! from django.test import TestCase from proj.schema import schema class TestUserModel(TestCase): def test_user_graphql(self): # Setup user = UserModel.objects.get(id=101) query = ''' query GetUser { user(id: "%s") { id name lastName } } ''' % b64_encoded_id(user.id) # Execute result = schema.execute(query) # Check self.assertEqual(result.errors, None) p = result.data['user'] self.assertEqual(p["lastName"], "Jones") app/tests.py

Slide 29

Slide 29 text

QUESTIONS? Mike Jones @imsickofmaps