Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Better APIs using GraphQL with Python and React by Mike Jones

7b0645f018c0bddc8ce3900ccc3ba70c?s=47 Pycon ZA
October 05, 2017

Better APIs using GraphQL with Python and React by Mike Jones

Have you ever found yourself stuck in a battle between creating more and more granular REST endpoints to return different payloads suitable for the individual use cases of different consumers and just Exposing All The Data, All The Time?

Enter GraphQL, born at Facebook but now in wide use in many organisation and with a burgeoning ecosystem of tools across many languages.

We'll be examining what GraphQL is, comparison to REST APIs, its potential use-cases and how to get productive quickly with Graphene (Python GraphQL service) on your backend and Apollo (Javascript client) on your frontend.

We'll use real-world examples from Picsa, where we've adopted Graphene-Django as the core element in our platform for building financial services for low-income farmworkers in South Africa.

This will be an introduction to these frameworks that should be applicable for those who have no prior experience.


Pycon ZA

October 05, 2017


  1. Better APIs using GraphQL with Python and React Mike Jones

  2. We help employers protect and improve the financial well‑being of

    their employees through innovative and impactful financial products.
  3. None
  4. WHY GRAPHQL? what’s wrong with REST? is this JS-fanboi NIH

    syndrome? I’m not Facebook Isn’t this just front-end stuff?
  5. WHAT PROBLEM DOES IT SOLVE? /api/clients/with_only_important /api/clients?fields=title,office Too many fat

    requests Smoother inter-team releases
  6. source: http://atomicdesign.bradfrost.com/chapter-2/ WHAT PROBLEM DOES IT SOLVE?

  7. WHAT IS IT NOT? Installable Language Specific A silver bullet

    Replacement for REST
  8. WHAT IS IT? Query Langage Nested Resources Specification for servers

    Self-documenting Typed

  10. query { me { firstName } } query { user(id:

    1) { firstName } } { "data": { "me": { "firstName": "Mike" } } } { "data": { "user": { "firstName": "Mike" } } }
  11. http://graphql.org/learn/queries/

  12. mutation { addUser(input: {firstName: “Dave”}) { user { firstName }

    } } { "data": { "addUser": { "user": { "firstName": "Dave" } } } }
  13. 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
  14. print 'hello world!' 1. Install graphene-django $ pyenv virtualenv 3.6.1

    proj $ pyenv shell proj $ pip install graphene-django
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. print 'hello world!' 7. Add config settings INSTALLED_APPS = (

    # ... 'graphene_django', ) GRAPHENE = { 'SCHEMA': 'proj.schema.schema' proj/settings.py
  21. print 'hello world!' 8. PROFIT! $ ./manage.py runserver $ open
  22. MAKING IT BETTER 1. Allow edits 2. Install django-filter and

    add some filters 3. Add some security 4. Add custom resolvers 5. Test
  23. 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
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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
  29. QUESTIONS? Mike Jones @imsickofmaps