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

GraphQL-first Django

GraphQL-first Django

GraphQL is a more flexible alternative to REST for building web APIs, and thus is becoming a strong foundation for any modern web stack. This is especially true where static HTML templates are not cutting it or a sophisticated single-page interface is needed, which is often the case on the web nowadays. Even though Django was designed as a model-view-template framework, it can work perfectly well as a GraphQL server to power JavaScript apps. This talk will elaborate on the anatomy of a GraphQL-first Django application, in which GraphQL queries and mutations are the primary interfaces exposed by the backend, while the frontend remains fully dynamic.

Marcin Gębala

September 19, 2020
Tweet

More Decks by Marcin Gębala

Other Decks in Programming

Transcript

  1. About me » Lead Developer at Saleor Commerce » Python,

    Django and GraphQL » Based in Wrocław, Poland @maarcingebala 2
  2. Saleor Saleor is an open-source headless e-commerce platform. » GraphQL

    API built with Django and Graphene (with over 350 operations) » Dashboard - management app for store owners (Typescript + React) » Storefront - template of an online shop (Typescript + React) saleor.io github.com/mirumee/saleor @maarcingebala 3
  3. Features » Fetching only data that is needed; the client

    decides what data to fetch » Three types of operations: queries, mutations, subscriptions » Combining multiple resources in a single request » Strong typing » ✨ Developer experience: interactive IDEs, code generation, API mocking ✨ @maarcingebala 7
  4. Schema type Query { products: [Product] } type Mutation {

    createProduct(input: ProductInput): Product } type Product { name: String! description: String createdAt: Date! categories: [Category] } input ProductInput { name: String! description: String categories: [ID] } » Definition of the API » Contract between frontend and backend » Schema-first vs. code- first approach @maarcingebala 8
  5. Graphene High-level framework for building GraphQL APIs in Python. »

    Rich ecosystem of libraries and integrations (Django, Flask, SQLAlchemy, Mongo) » Code-first approach - generating GraphQL schema from Python classes » The best Django integration of all GraphQL libraries in Python graphene-python.org github.com/graphql-python/graphene @maarcingebala 10
  6. Project structure » types.py - app's types; mapping models to

    GraphQL types » mutations.py - app's create/ update/delete operations » dataloaders.py - efficient data loading for queries and types » schema.py - schema of a single app » root schema.py - combine schema of all apps @maarcingebala 11
  7. Urls Single urls.py file with one view: # urls.py from

    django.urls import path from graphene_django.views import GraphQLView urlpatterns = [ path("graphql", GraphQLView.as_view(graphiql=True)), ] @maarcingebala 12
  8. Types # types.py from graphene_django import DjangoObjectType class Product(DjangoObjectType): price

    = graphene.Field(Money, description="Price of a product.") class Meta: description = "Represents a product." model = models.Product only_fields = [ "name", "description", "is_published", "updated_at", ] @staticmethod def resolve_price(root: models.Product, *_): return Money(amount=root.price, currency=settings.DEFAULT_CURRENCY) @maarcingebala 13
  9. Mutations # mutations.py class ProductCreate(graphene.Mutation): product = graphene.Field(types.Product) errors =

    graphene.List(Error, required=True, default_value=[]) class Arguments: input = ProductCreateInput(required=True) @classmethod def mutate(cls, root, info, input): product = models.Product(**input) try: product.full_clean() except ValidationError as e: errors = validation_error_to_error_type(e) return ProductCreate(errors=errors) product.save() return ProductCreate(product=product) @maarcingebala 15
  10. Form-based mutations Form mutations allow for generating mutations from forms.

    from graphene_django.forms.mutation import DjangoModelFormMutation class ProductCreateMutation(DjangoModelFormMutation): class Meta: form_class = ProductForm input_field_name = "input" return_field_name = "product" @maarcingebala 16
  11. Authentication JSON Web Token authentication is provided by django-graphql-jwt package:

    mutation { tokenCreate(email: "[email protected]", password: "secret") { token refreshToken user { id email } } } Authenticating requests with the Authorization header: { "Authorization": "JWT jwt-token-value" } @maarcingebala 17
  12. Permissions django-graphql-jwt also provides decorators to restrict access to particular

    fields in the schema. # types.py from graphql_jwt.decorators import permission_required class Product(DjangoObjectType): revenue = graphene.Field(Money) @permission_required("product.manage_products") def resolve_revenue(root: models.Product, *_): return get_product_revenue(root) @maarcingebala 18
  13. N+1 problem GraphQL query to return a list of products

    with categories, category is a foreign key in the Product model: { products { name category { name } } } Database queries: SELECT * FROM "products_product"; SELECT * FROM "products_category" WHERE "products_category"."id" = 1; SELECT * FROM "products_category" WHERE "products_category"."id" = 2; SELECT * FROM "products_category" WHERE "products_category"."id" = 3; ... SELECT * FROM "products_category" WHERE "products_category"."id" = N; @maarcingebala 19
  14. Data loaders Usage: class Product(DjangoObjectType): category = graphene.Field(Category, description="Product's category")

    @staticmethod def resolve_category(root: models.Product, info, *_): return CategoryByIdLoader(info.context).load(root.category_id) Loading a foreign key relation: from promise import Promise from promise.dataloader import DataLoader class CategoryByIdLoader(DataLoader): def batch_load_fn(self, keys): categories = models.Category.objects.in_bulk(keys) results = [categories.get(category_id) for category_id in keys] return Promise.resolve(results) @maarcingebala 20
  15. Testing Testing API operations with PyTest: PRODUCT_CREATE_MUTATION = """ mutation

    ProductCreate($slug: String, $name: String) { productCreate(input: { slug: $slug, name: $title }) { product { slug name } errors { message } } } """ def test_product_create_mutation(api_client): name = "T-shirt" slug = "t-shirt" variables = {"name": name, "slug": slug} response = api_client.post_graphql(PRODUCT_CREATE_MUTATION, variables) content = get_graphql_content(response) data = content["data"]["productCreate"] assert data["errors"] == [] assert data["product"]["name"] == name assert data["product"]["slug"] == slug @maarcingebala 22
  16. Real-time queries » Graphene-Django doesn't support subscriptions yet; there is

    no ASGI view/consumer to process WebSocket requests » Ongoing work on adding subscriptions support in Graphene v3 » Third-party libraries that add subscriptions support: » github.com/jaydenwindle/graphene-subscriptions » github.com/graphql-python/graphql-ws @maarcingebala 24
  17. Pros » Django = productivity. ORM and Graphene's code- first

    approach lets you progress fast » Familiar concepts: object types, form-based mutations, declarative style » Easy to add GraphQL API to an existing Django app » Example: Saleor GraphQL API built entirely in Django + Graphene @maarcingebala 26
  18. Cons » A fully-fledged server requires many additional libraries, which

    are not always well maintained » Lack of good learning resources » Uncertain roadmap of Graphene @maarcingebala 27
  19. Other libraries Ariadne - schema-first library for implementing GraphQL servers

    in Python. » fully-asynchronous; support for subscriptions » provides WSGI and ASGI views for Django » active community on Spectrum ariadnegraphql.org @maarcingebala 28
  20. Resources » Books: » "Learning GraphQL" by Eve Porcello, Alex

    Banks » "Production Ready GraphQL" by Marc-Andre Giroux » Tutorials: » Fullstack Apollo tutorial - apollographql.com/docs/tutorial/ introduction » How to GraphQL - howtographql.com » Example implementations: » Saleor GraphQL API: github.com/mirumee/saleor @maarcingebala 30