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

GraphQL in Python

Marcin Gębala
October 24, 2024
12

GraphQL in Python

GraphQL is establishing itself as a foundation of the modern web development stack, particularly where a dynamic, single-page UI is required. It unlocks many great benefits, some of which are:

- Fetching only the required data necessary to render particular views, while eliminating the need to call and combine data from multiple endpoints.
- Developer experience through incredible out-of-the-box tooling available - interactive API explorers or code generators for statically typed languages used in the frontend.
- Ability to combine various APIs under a single gateway with federations.

There are multiple libraries and approaches to build GraphQL APIs in Python. In this talk, we will look at two different approaches with popular libraries: schema-first approach with Ariadne (https://github.com/mirumee/ariadne) and code-first approach with Graphene (https://github.com/graphql-python/graphene). We'll take a look at the architecture and different aspects of a Python web app that serves a GraphQL API:

- Basics of GraphQL - the most essential benefits, how it differs from REST, the main concepts, and examples.
- Schema-first approach with Ariadne - how to design the schema and implement queries and mutations to interact with our data.
- Code-first approach with Graphene - how to represent types and operations with classes, examples of a large production web app built with Django and Graphene, mapping Django models to GraphQL types.
- Common web-app recipes - how to deal with authentication, permissions, or database performance in a GraphQL-first backend app.

Marcin Gębala

October 24, 2024
Tweet

Transcript

  1. About me » Principal Developer at Saleor Commerce (saleor.io) »

    Specialize in web development using Python, Django and GraphQL » Based in Wrocław, Poland ! Marcin Gębala - PyCon APAC 2024
  2. GraphQL GraphQL is a query language for APIs and a

    runtime for fulfilling those queries with your existing data. » Alternative to REST » Fetching only data that is needed » Combining resources in a single request » Strong typing » Backend agnostic » Open-source Marcin Gębala - PyCon APAC 2024
  3. Use-cases » Building APIs for web and mobile apps »

    Works great with single-page apps! » Apollo Client + React + Typescript » Code generation » Federations » Combining multiple services under one gateway » Communication between microservices - possible but not that common and practical Marcin Gębala - PyCon APAC 2024
  4. Components of a GraphQL API » Schema - defines the

    structure of the API » Resolvers - functions that return data for queries and mutations def resolve_user(obj, info, **kwargs): user_id = kwargs["id"] return db.get_user_by_id(user_id) schema { query: Query mutation: Mutation } type Query { users: [User!]! user(id: ID!): User posts: [Post!]! } type Mutation { createPost(input: PostData!): CreatePostResponse! } type User { id: ID! email: String! name: String } type Post { id: ID! user: User! content: String! createdAt: String! } input PostData { user: ID! content: String! } type CreatePostResponse { error: String post: Post } Marcin Gębala - PyCon APAC 2024
  5. Schema-first vs code-first Two approaches to building GraphQL APIs: -

    Schema-first - explicitly define the schema, then implement the resolvers to comply with the schema - Code-first - define the schema by writing classes/functions, then generate the schema from the code Marcin Gębala - PyCon APAC 2024
  6. Ariadne Library for building GraphQL servers in Python. » Schema-first

    » Lightweight and simple » Asynchronous » Supports GraphQL modules and generating Python API clients github.com/mirumee/ariadne Marcin Gębala - PyCon APAC 2024
  7. Schema definition from ariadne import make_executable_schema, MutationType, ObjectType, QueryType type_defs

    = """ type Query { user(id: ID!): User } type Mutation { createPost(userId: ID!, content: String!): Post! } type User { id: ID! email: String! name: String posts: [Post!]! } type Post { id: ID! user: User! content: String! createdAt: String! } """ mutation = MutationType() query = QueryType() post = ObjectType("Post") user = ObjectType("User") schema = make_executable_schema(type_defs, mutation, query, user, post) Marcin Gębala - PyCon APAC 2024
  8. Resolvers @query.field("user") def resolve_user(*_, **kwargs): user_id = kwargs["id"] return db.get_user_by_id(user_id)

    @mutation.field("createPost") def resolve_create_post(*_, userId, content): error = None post = None try: post = db.create_post(userId, content) except Exception as e: error = str(e) return post Marcin Gębala - PyCon APAC 2024
  9. Running the app Ariadne provides both ASGI and WSGI application

    that can be mounted in any ASGI-compatible (WSGI-compatible) framework. from ariadne.asgi import GraphQL from fastapi import FastAPI from .schema import schema app = FastAPI() app.mount("/graphql/", GraphQL(schema=schema)) Marcin Gębala - PyCon APAC 2024
  10. Graphene High-level framework for building GraphQL APIs in Python. »

    Code-first approach - generating GraphQL schema from Python classes » Rich ecosystem of libraries and integrations (Django, Flask, SQLAlchemy, Mongo) graphene-python.org github.com/graphql-python/graphene Marcin Gębala - PyCon APAC 2024
  11. Queries from graphene import ObjectType, Schema class Query(ObjectType): user =

    graphene.Field(User, id=graphene.ID(required=True)) users = graphene.List(types.User) @staticmethod def resolve_user(root, info, id): return models.User.objects.filter(id=id).first() @staticmethod def resolve_users(root, info): return models.User.objects.all() schema = Schema(query=Query) Marcin Gębala - PyCon APAC 2024
  12. Types from graphene import DjangoObjectType class User(DjangoObjectType): credit_card_number = graphene.Field(String,

    description="User's credit card number.") class Meta: description = "Represents a user." model = models.User only_fields = [ "created_at", "email", "profile_picture_url", ] @staticmethod def resolve_credit_card_number(root: models.User, *_): # Custom resolver to anonymize the credit card number. return anonymize_credit_card(root.credit_card) Marcin Gębala - PyCon APAC 2024
  13. Mutations class UserCreate(graphene.Mutation): user = graphene.Field(types.User) errors = graphene.List(Error, required=True,

    default_value=[]) class Arguments: input = UserCreateInput(required=True) @classmethod def mutate(cls, root, info, input): user = models.User(**input) try: user.full_clean() except ValidationError as e: errors = validation_error_to_error_type(e) return UserCreate(errors=errors) user.save() send_activation_email(user) return UserCreate(user=user) Marcin Gębala - PyCon APAC 2024
  14. Serving the schema Expose the API using Django views: from

    django.urls import path from graphene_django.views import GraphQLView urlpatterns = [ path("graphql", GraphQLView.as_view()), ] Marcin Gębala - PyCon APAC 2024
  15. 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" } Marcin Gębala - PyCon APAC 2024
  16. 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 User(DjangoObjectType): credit_card_number = graphene.Field(String, description="User's credit card number.") @permission_required("user.manage_users") def resolve_credit_card_number(root, info): return anonymize_credit_card(root.credit_card) Marcin Gębala - PyCon APAC 2024
  17. N+1 problem Without optimization, the following query could result in

    duplicated database queries: { posts { name user { email } } } SELECT * FROM "blog_post"; SELECT * FROM "user" WHERE "blog_post"."id" = 1; SELECT * FROM "user" WHERE "blog_post"."id" = 2; SELECT * FROM "user" WHERE "blog_post"."id" = 3; ... SELECT * FROM "user" WHERE "blog_post"."id" = N; Marcin Gębala - PyCon APAC 2024
  18. Data loaders Lazy loading of related objects using data loaders.

    class Post(DjangoObjectType): user = graphene.Field(User, description="Author of the post.") @staticmethod def resolve_user(root: models.Product, info, *_): return UserByIdLoader(info.context).load(root.user_id) from promise import Promise from promise.dataloader import DataLoader class UserByIdLoader(DataLoader): def batch_load_fn(self, user_ids): # in_bulk creates a dictionary of {id: <User>, ...} users = models.User.objects.in_bulk(keys) results = [users.get(user_id) for user_id in user_ids] return Promise.resolve(results) Marcin Gębala - PyCon APAC 2024
  19. Versioning » By design GraphQL APIs doesn't use versioning. »

    Evolve the schema by adding new fields, types and queries. » Use deprecation mechanism for backward-compatibility type Query { user(id: ID!): User @deprecated(reason: "Use `userById` instead.") userById(id: ID!): User } Marcin Gębala - PyCon APAC 2024
  20. Resources » Books: » "Learning GraphQL" by Eve Porcello, Alex

    Banks » "Production Ready GraphQL" by Marc-Andre Giroux » Example implementations: » Saleor GraphQL API: github.com/saleor/saleor » Ariadne demo: github.com/maarcingebala/ariadne-demo » Libraries: » Graphene: graphene-python.org » Ariadne: ariadnegraphql.org Marcin Gębala - PyCon APAC 2024